How to Install Umami Analytics on Docker (2026 Guide)
Learn how to install Umami Analytics with Docker Compose on your VPS. Step-by-step guide to self-host this privacy-focused Google Analytics alternative.

Umami is a simple, fast, privacy-focused web analytics tool and a solid Google Analytics alternative. It’s MIT-licensed, has over 37,000 GitHub stars, and collects only the metrics you need without tracking your visitors. If you want to compare options, Plausible and Matomo are similar self-hosted analytics tools. You can also install Plausible Analytics with a similar Docker setup.
With Google Analytics 4 and GDPR enforcement in the EU, self-hosted web analytics has become the default for operators who want full data ownership. Google Analytics scripts are heavy and slow down your site. Umami adds less than 2KB of overhead.
This guide covers how to install Umami Analytics on Docker with Docker Compose, including tracker setup, ad-blocker bypass, upgrades, and backups. Umami v3 shipped in November 2025 with a new UI, heatmaps, session replay, and it’s now PostgreSQL-only (MySQL support was dropped).
Umami v3 (November 2025)
Umami v3 is a major release with a redesigned UI, Segments, Cohorts, Session Replay, Heatmaps, and Web Vitals tracking. MySQL is no longer supported. PostgreSQL is the only database option. The compose files in this guide are updated for v3. See the Umami v3 blog post for full details.
You can find more free open source self-hosted apps at toolhunt.net self hosted section.
Steps to install Umami Analytics with Docker Compose
We’ll deploy Umami on a VPS with Docker Compose, then configure Cloudflare Tunnels for public access with SSL. The video walkthrough below covers the full process.
1. Prerequisites
- VPS with Docker and Docker Compose v2 installed. A Hetzner CX22 at ~€4/mo is sufficient (Umami + Postgres use ~300MB RAM total). Hostinger is another affordable option.
- Dockge or any Docker management tool, or just plain
docker compose. See the Dockge install guide for a full walkthrough. You can also check other self-hosted server panels. - Cloudflare Tunnel configured for your VPS (or any reverse proxy with SSL termination)
- A domain or subdomain ready for Umami
- PostgreSQL v12.14+ (we use v15-alpine, which satisfies this)
Don't use 'analytics' as your subdomain
Ad-blockers commonly block requests to analytics.yourdomain.com. Use something like stats, metrics, or track instead. This applies to the subdomain AND the script path.
2. Docker Compose file for Umami
Below are two compose files. The first is a clean setup with no backup, use this if you plan to back up with a cron-based pg_dump (recommended). The second adds a tiredofit/db-backup sidecar for automatic scheduled dumps.
services:
umami:
image: docker.umami.is/umami-software/umami:postgresql-latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@umami-db:5432/${POSTGRES_DB}
APP_SECRET: ${APP_SECRET}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY:-1}
depends_on:
umami-db:
condition: service_healthy
init: true
restart: always
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
interval: 5s
timeout: 5s
retries: 5
umami-db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: UTC
volumes:
- ./umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5The Umami service runs on port 3000, connects to PostgreSQL over the internal Docker network, and includes a health check hitting /api/heartbeat. The Postgres data lives in a local volume at ./umami-db-data. You can change the port mapping to whatever you want.
services:
umami:
image: docker.umami.is/umami-software/umami:postgresql-latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@umami-db:5432/${POSTGRES_DB}
APP_SECRET: ${APP_SECRET}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY:-1}
depends_on:
umami-db:
condition: service_healthy
init: true
restart: always
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
interval: 5s
timeout: 5s
retries: 5
umami-db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: UTC
volumes:
- ./umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
umami-db-backup:
container_name: umami-db-backup
image: tiredofit/db-backup
volumes:
- ./backups:/backup
environment:
DB_TYPE: postgres
DB_HOST: umami-db
DB_NAME: ${POSTGRES_DB}
DB_USER: ${POSTGRES_USER}
DB_PASS: ${POSTGRES_PASSWORD}
DB_BACKUP_INTERVAL: 720
DB_CLEANUP_TIME: 72000
CHECKSUM: SHA1
COMPRESSION: GZ
CONTAINER_ENABLE_MONITORING: false
depends_on:
umami-db:
condition: service_healthy
restart: alwaysThe backup sidecar dumps the database every 12 hours and cleans up backups older than 50 days. Dumps land in ./backups. The tiredofit/db-backup image is heavier than a simple cron. If you want something lighter, see the backup and restore section for a one-liner pg_dump alternative.
Pin your image version
postgresql-latest now points to Umami v3 (currently v3.2.0 as of June 2026). For reproducible deploys, pin to a specific tag like 3.2.0. Note that v3 tags dropped the v prefix. It’s 3.2.0, not v3.2.0. See Docker Hub tags for all available versions. The image is also available on GHCR (ghcr.io/umami-software/umami:latest) and Docker Hub (umamisoftware/umami:postgresql-latest).
If you need to run multiple PostgreSQL databases on the same host, you can consolidate them into a single Postgres container to save memory.
3. Configure the .env file
Create a .env file in the same directory as your compose file (or add the variables in Dockge’s environment section).
POSTGRES_USER=umami
POSTGRES_PASSWORD=your-secure-password
POSTGRES_DB=umami
APP_SECRET=run-openssl-rand-hex-32-to-generate
DISABLE_TELEMETRY=1
Generate a unique APP_SECRET
Never use the example value. Generate a secure secret with:openssl rand -hex 32
Paste the output as your APP_SECRET. This is used for session encryption.
4. Deploy Umami
If using Dockge, add a name for your stack and hit deploy. With plain Docker Compose:
docker compose up -d
Verify both containers are healthy:
docker compose ps
You should see both umami and umami-db with status healthy. Check the Umami logs to confirm migrations ran:
docker compose logs -f umami
Healthy output looks like: All migrations have been successfully applied followed by the server listening on port 3000.
Access Umami at http://your-vps-ip:3000. Log in with the default credentials:
- Username:
admin - Password:
umami
Change the default password now
The default credentials admin / umami are public knowledge. Go to Settings → Profile → Change password immediately, before exposing Umami to the internet.
Quick verification with curl:
curl http://localhost:3000/api/heartbeat
# Should return: OK
5. Configure Cloudflare Tunnels for Umami
Go to Access → Tunnels in the Cloudflare dashboard, choose your tunnel, and add a hostname mapping a domain or subdomain to the Umami service on port 3000.

Getting 'Unknown' visitor IPs?
If Umami shows “Unknown” for visitor locations behind Cloudflare Tunnels, add CLIENT_IP_HEADER=cf-connecting-ip to the umami service environment block in your compose file. This tells Umami to read the real client IP from Cloudflare’s header. Restart the container after the change.
You can also check Setup CloudPanel as Reverse Proxy with Docker and Dockge to use CloudPanel as a reverse proxy to your Docker containers, or self-host with Docker and Cloudflare Tunnels for another deployment example.
6. Add your first website to Umami
After the tunnel is configured, access Umami at your domain. Log in and go to Settings in the header. You’ll see a button to add a website:

After adding a website, click Edit Website (or go to Settings) to find your tracking code. You can paste this into your site’s <head>.

Umami tracking code and tracker configuration
Once your website is added, Umami gives you a tracking script. Here’s how to use it and what options are available.
Basic tracking script
The standard embed looks like this:
<script
defer
src="https://your-umami-domain.com/script.js"
data-website-id="your-website-id"
></script>
data-website-id(the UUID of the website you added in Umami, visible in the tracking code snippet)data-host-url(set this if your tracker domain differs from the dashboard domain, e.g., you serve the script from a CDN)
Custom events and advanced options
Umami supports both programmatic and declarative event tracking.
Programmatic (call from JavaScript):
// Track a custom event with properties
umami.track("signup", { plan: "pro", source: "header" });
Declarative (add HTML attributes to any element):
<button data-umami-event="click-download" data-umami-file="whitepaper.pdf">
Download
</button>
Other useful data attributes:
| Attribute | Purpose |
|---|---|
data-domains="example.com,api.example.com" |
Restrict tracking to specific domains |
data-do-not-track="true" |
Respect the browser’s DNT header |
data-auto-track="false" |
Disable automatic pageview tracking (useful for SPAs that handle their own routing) |
data-performance="true" |
Track Core Web Vitals (new in v3.1.0) |
data-auto-pageview="false" |
Suppress automatic pageview on script load (new in v3.2.0) |
If you’re using an Astro site, see Plausible Analytics for Astro with Cloudflare Workers for a related analytics integration approach.
Bypass ad blockers
Ad-blockers target two default paths: /script.js and /api/send. You can rename both with environment variables:
TRACKER_SCRIPT_NAME=custom-tracker
COLLECT_API_ENDPOINT=/api/collect
Add these to the umami service environment in your compose file, then restart. Update your tracking script src to match:
<script
defer
src="https://your-domain.com/custom-tracker.js"
data-website-id="your-website-id"
></script>
Ad-blocker bypass requires compose changes
After adding TRACKER_SCRIPT_NAME and COLLECT_API_ENDPOINT to your compose file, restart the Umami container: docker compose up -d –force-recreate umami. Make sure any reverse proxy rules pass through the new endpoint path.
Full ad-blocker bypass example
Here’s a complete snippet to add to the umami service environment in your compose file:
environment:
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@umami-db:5432/${POSTGRES_DB}
APP_SECRET: ${APP_SECRET}
DISABLE_TELEMETRY: ${DISABLE_TELEMETRY:-1}
TRACKER_SCRIPT_NAME: my-stats
COLLECT_API_ENDPOINT: /api/c
CLIENT_IP_HEADER: cf-connecting-ipThen in your site’s HTML:
<script
defer
src="https://stats.yourdomain.com/my-stats.js"
data-website-id="your-website-id"
></script>If you use a reverse proxy (Nginx, Caddy, Traefik), ensure the custom paths are forwarded to the Umami container on port 3000. No extra routing config is needed if the proxy sends all traffic to Umami.
How to update Umami Analytics on Docker
Standard updates (same major version)
For minor updates within v3.x:
# 1. Back up the database first
docker compose exec -T umami-db pg_dump -U umami umami | gzip > backups/umami-$(date +%F-%H%M).sql.gz
# 2. Pull the latest image
docker compose pull
# 3. Recreate containers
docker compose up -d --force-recreate
# 4. Verify: look for "All migrations have been successfully applied"
docker compose logs -f umami
Upgrading from Umami v2 to v3
Breaking changes in Umami v3
v3 drops MySQL support, has a completely new UI, and runs schema migrations that can take time on large databases. Always back up before upgrading. If you were on MySQL, follow the MySQL to PostgreSQL migration guide first.
Steps:
# 1. Back up
docker compose exec -T umami-db pg_dump -U umami umami > umami-backup-$(date +%F-%H%M).sql
# 2. Pull and recreate
docker compose pull
docker compose up -d --force-recreate umami
# 3. Watch logs for migration output
docker compose logs -f umami
# 4. Run ANALYZE on PostgreSQL (critical after major upgrades)
docker compose exec -T umami-db psql -U umami umami -c "ANALYZE;"
The ANALYZE command updates PostgreSQL’s query planner statistics after schema migrations. The official Umami docs recommend this. Without it, dashboard queries can become slow after a major upgrade.
After upgrading, old Docker images accumulate on disk. You can clean up Docker disk space to reclaim storage.
Database backup and restore
Always back up before upgrading
A failed migration can corrupt data. Always run pg_dump before pulling a new major version.
Backup (one-liner, can be cron’d):
docker compose exec -T umami-db pg_dump -U umami umami | gzip > backups/umami-$(date +%F).sql.gzAdd to crontab for daily backups at 3 AM:
0 3 * * * cd /path/to/umami && docker compose exec -T umami-db pg_dump -U umami umami | gzip > backups/umami-$(date +\%F).sql.gzRestore:
gunzip < backups/umami-YYYY-MM-DD.sql.gz | docker compose exec -T umami-db psql -U umami umamiFor offsite backups, push the compressed dump to any S3-compatible storage (MinIO, Cloudflare R2, Backblaze B2).
If you used the “With Backup” compose file, dumps are in the ./backups directory, created every 12 hours. To restore from a tiredofit backup:
# Find the latest backup
ls -lt backups/
# Restore it
zcat backups/umami-db-*.sql.gz | docker compose exec -T umami-db psql -U umami umamiThe tiredofit/db-backup sidecar is heavier than a cron-based approach (it runs its own scheduler, compression, and cleanup). For a minimal footprint, the manual pg_dump cron is lighter.
Verify and troubleshoot your Umami installation
docker compose ps: bothumamiandumami-dbshow statushealthydocker compose logs umami: shows “All migrations have been successfully applied”curl http://localhost:3000/api/heartbeat: returns OK- Login works with
admin/umami(change password immediately) - Embed the tracking script on a test page, open browser DevTools → Network tab, confirm
script.jsloads (200 OK) - Visit the test page, check Umami dashboard for a real-time visit
If you want to monitor your server resources alongside Umami, set up a lightweight monitoring stack.
Container keeps restarting
Check docker compose logs umami. The most common cause is a wrong DATABASE_URL or the database container isn’t healthy yet. Verify your .env variables match what the compose file references. The depends_on condition should wait for the DB health check, but if the DB is slow to start on a low-memory VPS, give it 30 seconds.
'service umami-db not found' error
A typo in depends_on. Make sure it says umami-db (with “m”), not unami-db. This was a bug in earlier versions of this guide’s compose file.
Blank or slow dashboard after upgrade
After a major version upgrade (especially v2 → v3), PostgreSQL’s query planner may have stale statistics. Run:
docker compose exec -T umami-db psql -U umami umami -c "ANALYZE;"Visitor IPs show as 'Unknown'
When Umami runs behind Cloudflare Tunnels or a reverse proxy, the client IP doesn’t reach it directly. Add to the umami service environment:
CLIENT_IP_HEADER=cf-connecting-ipUse x-forwarded-for if you’re behind Nginx or Traefik instead of Cloudflare. Restart the container after the change.
Tracking script blocked by ad-blockers
Default paths /script.js and /api/send are on blocklists. Use TRACKER_SCRIPT_NAME and COLLECT_API_ENDPOINT environment variables to rename them. See the bypass ad blockers section above.
Out of memory / OOM kills
Umami uses ~200MB RAM, Postgres ~50-100MB. A 1GB VPS is the practical minimum. Check resource usage with docker stats. If containers are getting OOM-killed, consider adding memory limits in compose:
deploy:
resources:
limits:
memory: 256MUmami v3: what’s new
| Version | Date | Highlights |
|---|---|---|
| v3.0.0 | Nov 2025 | New UI, Segments, Cohorts, Links, Pixels, Admin page. PostgreSQL-only (MySQL dropped). |
| v3.1.0 | Apr 2026 | Boards, Session Replay, Web Vitals, redesigned share page, OR filters, regex, funnels. Requires Node 22. |
| v3.2.0 | Jun 2026 | Heatmaps, improved Session Replay, event/session property reporting, data-auto-pageview attribute. |
Self-hosting vs Umami Cloud
Self-hosted Umami = unlimited events, full data ownership, free (MIT license). Umami Cloud starts at free (100K events/mo, 1 website) but the Pro plan is $20/mo for 1M events and 20 websites. For a solo operator running a few sites, self-hosting on a ~€4/mo VPS is the clear winner.
PostgreSQL v12.14+ is required for v3. The postgres:15-alpine image used in this guide satisfies this.
Conclusion
You now have Umami Analytics installed on Docker, with a working tracker configuration, ad-blocker bypass options, and a solid upgrade and backup workflow. Umami is free, uses about 300MB of RAM total, and gives you full ownership of your analytics data. No third-party access, no GDPR headaches.
Key things to remember: back up before every major upgrade, pin your image version for reproducibility, and change the default password immediately after first login. If you want to explore other options, you can also install Plausible Analytics with a similar Docker approach, or monitor your server resources to keep an eye on your VPS.
Explore More Self-Hosted Tools

