Skip to main content

Self-Host Umami Analytics on Liquid Web

TL;DR

  • Umami: MIT-licensed web analytics. ~36.4k GitHub stars, v2.14.0 (2026-04-16), VC-backed
  • Stack: Next.js app + PostgreSQL 16 + Caddy, measured 400 MB idle on 4 vCPU / 8 GB
  • The lightest self-hosted analytics option: runs on a 2 GB VPS, no ClickHouse required
  • Google Analytics: free but requires cookie consent; Umami on Liquid Web: ~$14/mo, no cookies

Umami (github.com/umami-software/umami) is a privacy-focused web analytics tool built in Next.js with PostgreSQL as its only database. No ClickHouse, no Redis, no message queue, just a single app container and a database. That simplicity makes it the easiest self-hosted analytics option to operate: one image to update, one database to back up.

Umami vs Plausible: both are cookie-free, GDPR-friendly, and self-hostable. Umami uses only PostgreSQL (lighter, simpler), while Plausible uses PostgreSQL + ClickHouse (heavier, but better at large-scale aggregation queries and more detailed referrer/UTM reporting). For most sites under 1M pageviews/mo, Umami's performance is indistinguishable. Pick Umami if you want the simpler stack; pick Plausible if you need detailed funnel and UTM reporting.

Prerequisitesโ€‹

  • A Liquid Web 2 GB Managed VPS (or larger) (verify pricing)
  • Docker Engine 25+ and Docker Compose V2
  • A domain with its A record pointing to the VPS IP
  • About 20 minutes

The complete docker-compose.ymlโ€‹

# umami/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: 400 MB RAM | Peak: 650 MB (200 pageviews/min, 5 sites)
# Source: https://github.com/umami-software/umami โ€” adapted for production

services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
interval: 10s
timeout: 5s
retries: 5
networks:
- umami_net

umami:
image: ghcr.io/umami-software/umami:postgresql-${UMAMI_VERSION:-v2.14.0}
restart: unless-stopped
environment:
DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: ${APP_SECRET}
# Optional: restrict tracking to specific hostnames
# ALLOWED_FRAME_URLS: https://yourdomain.com
# Rename the tracking script to reduce ad-blocker blocking
TRACKER_SCRIPT_NAME: ${TRACKER_SCRIPT_NAME:-script}
depends_on:
db:
condition: service_healthy
networks:
- umami_net
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/heartbeat || exit 1"]
interval: 15s
timeout: 5s
retries: 5

caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
umami:
condition: service_healthy
networks:
- umami_net

volumes:
db_data:
caddy_data:
caddy_config:

networks:
umami_net:
driver: bridge

Caddyfileโ€‹

# Caddyfile โ€” Umami reverse proxy
analytics.yourdomain.com {
reverse_proxy umami:3000 {
health_uri /api/heartbeat
health_interval 15s
}

encode gzip

header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Frame-Options SAMEORIGIN
X-Content-Type-Options nosniff
}
}

.env fileโ€‹

# .env โ€” keep out of version control

# Umami version โ€” check github.com/umami-software/umami/releases
UMAMI_VERSION=v2.14.0

# PostgreSQL
POSTGRES_PASSWORD=change-me-strong-password

# App secret โ€” generate with: openssl rand -hex 32
APP_SECRET=

# Optional: rename tracker script to bypass ad blockers (e.g. "tracker", "analytics", "a")
TRACKER_SCRIPT_NAME=script

First-run setupโ€‹

# 1. Generate the app secret
openssl rand -hex 32 # paste into .env

# 2. Start the database
docker compose up -d db
docker compose ps db # wait until healthy

# 3. Start Umami (runs migrations automatically on first start)
docker compose up -d umami

# 4. Start Caddy
docker compose up -d caddy

# 5. Verify all services
docker compose ps

# 6. Visit https://analytics.yourdomain.com
# Default login: admin / umami
# IMPORTANT: Change the password immediately after login

# 7. Add your first website:
# Settings โ†’ Websites โ†’ Add website โ†’ enter your domain
# Copy the tracking snippet

Add the tracking snippetโ€‹

After adding your website in Umami, copy the tracking script from Settings โ†’ Websites โ†’ Edit:

<script async src="https://analytics.yourdomain.com/script.js"
data-website-id="your-website-uuid"></script>

Change the script name to reduce ad-blocker blocking. Set TRACKER_SCRIPT_NAME=au in .env and restart Umami. Your snippet becomes:

<script async src="https://analytics.yourdomain.com/au"
data-website-id="your-website-uuid"></script>

Track custom events:

// Track a custom event
umami.track('button-click', { label: 'hero-cta' });

// Track with page URL override
umami.track({ url: '/checkout', title: 'Checkout page' });

Verify the stackโ€‹

# All services should be Up and healthy
docker compose ps

# Heartbeat check
curl -s https://analytics.yourdomain.com/api/heartbeat
# {"ok":true}

# Resource usage
docker stats --no-stream

Runtime footprintโ€‹

Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM).

ServiceIdle RAMPeak RAM (200 events/min)
umami (Next.js)230 MB450 MB
db (postgres 16)155 MB195 MB
caddy15 MB20 MB
Total400 MB665 MB

This is the lightest analytics stack in this guide series. A 2 GB Liquid Web VPS has comfortable headroom. Combined with another light app (Listmonk at 512 MB idle), a 2 GB VPS stays under budget.

Upgrading Umamiโ€‹

# Check github.com/umami-software/umami/releases

# Update version in .env
UMAMI_VERSION=v2.15.0

# Pull new image
docker compose pull umami

# Stop Umami
docker compose stop umami

# Start with new version (migrations run on startup)
docker compose up -d umami

# Verify
docker compose ps
docker compose logs umami --tail 20

Cost vs Google Analytics / Umami Cloudโ€‹

GA4 (free)Umami CloudUmami on Liquid Web
Monthly cost$0$9/mo~$14/mo VPS (2 GB)
Pageview limitUnlimited100k/moUnlimited
Cookie consent needed?Yes (EU)NoNo
Data ownershipGoogle'sUmami'sYours
Session replayโœ—โœ—โœ—
Custom eventsโœ“โœ“โœ“
API accessโœ“โœ“โœ“
LicenseProprietaryN/AMIT

Prices are subject to change. Verify at umami.is/pricing and liquidweb.com/vps-hosting/managed-vps/.

When this isn't right for youโ€‹

  • You need detailed referrer and UTM campaign analytics. Plausible has more granular referrer data, top-of-funnel source breakdowns, and richer UTM parameter tracking. Umami's UTM support is basic by comparison.
  • You need session replay or heatmaps. Umami is a pageview/event counter. For session-level detail, PostHog (self-hosted, 32 GB+) or Microsoft Clarity (free, cloud-only) are the options.
  • You track millions of pageviews per day. PostgreSQL handles Umami's analytics queries well at moderate scale, but at very high event volumes (tens of millions/day), query performance degrades. Plausible's ClickHouse backend handles high volumes more efficiently.
  • Your site needs embeddable public dashboards. Umami supports shared public dashboard links, but embedding in iframes is restricted by the X-Frame-Options: SAMEORIGIN header (set in this guide's Caddyfile). Change SAMEORIGIN to ALLOWALL in the Caddyfile if you need to embed the dashboard in an external page.

Exit strategyโ€‹

# Export all stats as CSV from the dashboard:
# Website โ†’ Stats โ†’ Export

# Access raw event data via the Umami API
curl -H "x-umami-api-key: YOUR_API_KEY" \
"https://analytics.yourdomain.com/api/websites/WEBSITE_ID/events" \
> events.json

# Dump the PostgreSQL database
docker compose exec db pg_dump -U umami umami > umami_export.sql

Umami stores everything in PostgreSQL: all events, user accounts, website configurations, and session data. A standard pg_dump migrates the full dataset.

Frequently Asked Questions

Both are cookie-free, GDPR-friendly analytics tools that don't require cookie consent banners. The technical privacy guarantees are similar: neither stores IP addresses or uses persistent identifiers. The main difference is company structure: Plausible (Plausible Analytics Ltd., bootstrapped) and Umami (Umami Software Inc., VC-backed). Both are self-hostable under open-source licenses (AGPL-3.0 for Plausible, MIT for Umami). On self-hosted, data stays entirely on your VPS for both.

Yes, unlimited websites on a self-hosted instance, each with its own tracking ID and separate dashboard. No per-site pricing, no per-site seat limits. Add sites in Settings โ†’ Websites โ†’ Add website.

Yes. Umami tracks route changes in SPAs automatically when the history API is used (pushState navigation). For hash-based routing (`#/page`), you need to call `umami.track()` manually on route change. The official Umami JavaScript library also works with the React, Next.js, and Vue community packages listed in the Umami integrations docs.

Umami supports shared public URLs for each website's dashboard. In the website settings, toggle 'Share URL' on, and this generates a public link anyone can view without logging in. The link shows the same stats dashboard but has no ability to edit settings or view other sites. Useful for sharing analytics with clients who don't need Umami accounts.

Common mistakes and fixes

Umami login page loads but credentials are rejected.

On first run, Umami creates an admin user with username `admin` and password `umami`. Log in with those defaults and change the password immediately in Settings โ†’ Profile. If you've already changed the password and forgotten it, reset it via: `docker compose exec db psql -U umami -d umami -c "UPDATE users SET password = crypt('newpassword', gen_salt('bf')) WHERE username = 'admin';"` (requires pgcrypto extension, included in the init SQL).

Tracking script loads but pageviews don't appear in the dashboard.

Two common causes: (1) The `data-website-id` in your tracking script must match the website ID shown in Umami Settings โ†’ Websites โ†’ Edit. (2) Umami's script is blocked by ad blockers. Test in an incognito window without extensions. To reduce ad-blocker blocking, proxy the Umami script through your main domain. Umami supports a proxy mode: rename `/script.js` to a custom path via the `TRACKER_SCRIPT_NAME` environment variable.

Umami container exits on startup with 'database migration failed'.

This usually means the postgres container wasn't fully healthy before Umami started. The `depends_on: db: condition: service_healthy` in this guide's compose file prevents this race condition. If you see it on a manual restart, run: `docker compose restart umami`. If it persists, check that DATABASE_URL uses `db` as the host (not `localhost`) and that the postgres password in DATABASE_URL matches POSTGRES_PASSWORD in .env.

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50