How to Install Umami Analytics on Ubuntu 24.04 — A Privacy-First Google Analytics Alternative
Umami is the simplest way to get clean, privacy-respecting website analytics without feeding a third-party ad network or adding a cookie banner. This guide walks you through a production-grade Umami v2 install on Ubuntu 24.04 — from fresh VPS to TLS-secured dashboard.
Skip the setup? Deploy Umami in one click. Launch a CloudCore Starter VPS and have your first dashboard live in under 90 seconds.
Table of Contents
What is Umami?
Umami is a simple, fast, open-source web analytics platform released under the permissive MIT license. It gives you the numbers that matter — unique visitors, pageviews, referrers, countries, devices, and custom events — without cookies, without fingerprinting, and without handing visitor data to Google.
Compared to Plausible, Umami is simpler to operate, stays fully free as MIT, and needs no license key for commercial use. Compared to Matomo, it is dramatically lighter. Because Umami collects only anonymous, aggregated data and sets no cross-site cookies, you do not need a GDPR cookie banner.
Umami v2 Features
Umami v2 is a substantial rewrite of the original v1:
- Unlimited websites, each with its own tracking ID and dashboard.
- Teams — invite users, assign roles (owner, admin, view-only), scope access per team.
- Reports — Insights, Funnel, Retention, UTM, Goals, and User Journey.
- Shared views — public URLs to share any dashboard without giving stakeholders an account.
- Custom events and properties — track clicks, form submits, signups with arbitrary metadata.
- UTM tracking — full
utm_source,utm_medium,utm_campaign,utm_term,utm_content. - API access — bearer-token REST API for reading data and managing websites.
- Realtime dashboard and a small Next.js footprint (~200 MB RAM at idle).
Why Self-Host Umami?
Umami Cloud Free caps at 10K events/month and 3 websites. Self-hosting wins when:
- You have more than 10K monthly events — the cloud free tier runs out quickly.
- You run multiple client sites — no per-website limits.
- You want full data ownership — every row lives in your own Postgres.
- You need to bypass ad blockers — only self-hosted can serve the tracker from your own domain.
- You are under data-residency rules (EU, healthcare, finance).
Cost Comparison
| Scenario | GA4 | Umami Cloud Pro | Plausible Cloud | Self-Hosted Umami |
|---|---|---|---|---|
| Monthly cost | Free | from $20/mo | from $9/mo | EUR 7.99/mo (VPS) |
| Cookie banner | Yes | No | No | No |
| Data leaves your server? | Yes | Yes | Yes | No |
| Event limit | Soft/sampled | Per tier | Per tier | Disk space |
| Websites | Unlimited | 3+ | 10+ | Unlimited |
Architecture
Umami v2 is a small, predictable stack:
- Umami app — a Next.js application serving both the admin UI and the
/api/sendtracking endpoint on port 3000. - Database — PostgreSQL 12+ (recommended) or MySQL 8+ for websites, users, sessions, and events.
- Tracker — a ~2 KB JS file at
/script.js.
Prerequisites
You need:
- Ubuntu 24.04 LTS VPS with root or sudo access and SSH
- 2 vCPU, 2 GB RAM, 20 GB disk minimum
- A domain (e.g.
analytics.yourdomain.com) with an A record to your VPS IP - Ports 80/443 open for Let's Encrypt and the dashboard
Recommended Plan: CloudCore Starter>
For production Umami tracking up to several million events per month, we recommend CloudCore Starter: 2 vCPU, 2 GB RAM, 40 GB NVMe, unmetered bandwidth, EUR 7.99/month. Enough headroom for the app, Postgres, Nginx, and backups.
Connect to your server via SSH:
ssh root@your-server-ipStep 1: Update System Packages
Bring the system up to date:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl ca-certificates gnupg ufwReboot if the kernel was upgraded.
Step 2: Install Docker and Docker Compose
Umami is distributed as a Docker image. Use the official docker.com repository — not the distro package. For full instructions see How to Install Docker on Ubuntu 24.04. Quick summary:
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp dockerVerify:
docker --version
docker compose versionExpected output:
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7Step 3: Create the docker-compose.yml
Create a dedicated directory for the Umami stack:
sudo mkdir -p /opt/umami
cd /opt/umamiGenerate a secure APP_SECRET. Umami uses this to sign session tokens — keep it private and back it up:
openssl rand -base64 48Copy the resulting string; you will paste it into the compose file in a moment.
Create /opt/umami/docker-compose.yml:
services: umami: image: ghcr.io/umami-software/umami:postgresql-latest container_name: umami restart: unless-stopped ports: - "127.0.0.1:3000:3000" environment: DATABASE_URL: postgresql://umami:CHANGE_ME_STRONG_DB_PASSWORD@db:5432/umami DATABASE_TYPE: postgresql APP_SECRET: PASTE_YOUR_OPENSSL_RAND_OUTPUT_HERE PORT: 3000 depends_on: db: condition: service_healthy
db: image: postgres:15-alpine container_name: umami-db restart: unless-stopped environment: POSTGRES_DB: umami POSTGRES_USER: umami POSTGRES_PASSWORD: CHANGE_ME_STRONG_DB_PASSWORD volumes: - ./pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U umami -d umami"] interval: 10s timeout: 5s retries: 5
Notes on that file:
127.0.0.1:3000:3000— Umami listens only on loopback until Nginx proxies it (Step 11).postgres:15-alpine— Umami supports Postgres 12-16; 15 is the current sweet spot.APP_SECRET— rotating this invalidates all existing sessions.
CHANGE_ME_STRONG_DB_PASSWORD values with the same generated password (openssl rand -base64 24) and paste your APP_SECRET.Step 4: Start Umami and Run Migrations
Bring the stack up:
docker compose up -dExpected output:
[+] Running 3/3
✔ Network umami_default Created
✔ Container umami-db Started
✔ Container umami StartedDatabase migrations run automatically on first boot. Tail the logs:
docker compose logs -f umamiLook for Applying migration ... followed by ✓ Ready in 2.1s, then Ctrl+C to stop following.
Confirm the app responds locally:
curl -I http://127.0.0.1:3000Expected: HTTP/1.1 200 OK.
Step 5: First Login and Password Change
Umami ships with a default administrator: username admin, password umami.
The dashboard is only on 127.0.0.1:3000. To log in from your laptop, open an SSH tunnel:
ssh -L 3000:127.0.0.1:3000 root@your-server-ipBrowse to http://localhost:3000, enter admin / umami, and immediately change the password under user menu → Profile → Change password. Pick 20+ characters. There is no built-in 2FA yet.
Step 6: Add a Website and Install the Tracking Script
In the Umami dashboard, navigate to Settings → Websites → Add website. Fill in Name and Domain (root hostname, no scheme or path). Click Save — Umami generates a Website ID (UUID).
Click Edit on the new website, then the Tracking code tab:
<script
defer
src="https://analytics.yourdomain.com/script.js"
data-website-id="b4e8b6c8-1a2d-4f9c-8e20-1234567890ab"></script>Standard install
Paste this snippet into <head> on every tracked page (Next.js root layout, WordPress header plugin, etc.). The defer attribute ensures it never blocks rendering.
Restrict to specific hostnames
To keep preview/staging environments out of the data, add data-domains:
<script
defer
src="https://analytics.yourdomain.com/script.js"
data-website-id="b4e8b6c8-1a2d-4f9c-8e20-1234567890ab"
data-domains="example.com,www.example.com"></script>Events from any other hostname are silently discarded.
Other useful attributes
data-auto-track="false"— disable automatic pageview tracking (useful for SPA routers).data-do-not-track="true"— respect the browser'sDNTheader.data-cache="true"— cache the session ID inlocalStorage.data-host-url="..."— override the destination host (useful behind a CDN).
Step 7: Track Custom Events and Properties
Pageviews track automatically. For buttons, form submits, and funnel milestones, call window.umami.track():
<button onclick="umami.track('signup-clicked')">Sign up</button>Or with custom properties:
document.querySelector('#checkout').addEventListener('click', () => {
umami.track('checkout-started', {
plan: 'cloudcore-starter',
currency: 'EUR',
value: 11.99,
});
});Custom properties appear under Events → Event data and can be filtered, grouped, and used as funnel steps.
Important limits
- Event names: 50 chars. Property keys: 50 chars. Values: 500 chars.
- Properties must be strings, numbers, or booleans (nested objects are flattened).
Step 8: Reports, Teams, and Shared Views
Open Reports → Create report and pick a type:
- Insights — slice any metric by any dimension (country, browser, referrer, UTM, custom property).
- Funnel — ordered sequence of events or URLs.
- Retention — cohort retention by day, week, or month.
- UTM — breakdown by
utm_source,utm_medium,utm_campaign,utm_term,utm_content. - Goals — count event or URL hits within a date range.
- Journey — Sankey visualization of visitor paths.
Teams
Under Settings → Teams, create a team and invite users as owner, manager, member, or view-only. One team per client is a clean agency pattern — invite each client as view-only so they see only their own numbers.
Shared public views
For stakeholders without accounts: open a website → Edit → Share URL → toggle Enable share URL, then send the URL. The shared view renders the dashboard without exposing settings, event IDs, or API keys.
Step 9: Build an Event Funnel
Suppose you want to measure a three-step signup funnel: pricing page → CTA click → signup completed.
Instrument the events in your app code:
document.querySelector('#cta-primary').addEventListener('click', () => {
umami.track('cta-clicked', { placement: 'hero' });
});
umami.track('signup-completed', { plan: selectedPlan });Create the funnel report. Go to Reports → Create report → Funnel:
/pricingcta-clickedsignup-completedUmami returns a stacked bar chart with drop-off percentages between steps. Save the report, add it to a dashboard, or share via public URL. Keep funnels to 3-5 steps.
Step 10: Bypass Ad Blockers with a CNAME
The default /script.js filename is not on major blocklists today, but some privacy tools block anything that smells like analytics. The most reliable countermeasure is to serve the tracker from a first-party subdomain of the tracked site.
Option A: Subdomain CNAME
Create a CNAME on the tracked site: stats.example.com CNAME analytics.yourdomain.com. Then load the tracker from the first-party subdomain:
<script
defer
src="https://stats.example.com/script.js"
data-website-id="b4e8b6c8-1a2d-4f9c-8e20-1234567890ab"
data-host-url="https://stats.example.com"></script>Make sure your Nginx config (Step 11) covers stats.example.com with its own Let's Encrypt cert.
Option B: Rename the script file
In Nginx, alias /script.js to a non-obvious path:
location = /pageloader.js {
proxy_pass http://127.0.0.1:3000/script.js;
}Reference src="https://stats.example.com/pageloader.js". Combined with the CNAME, the tracker is practically invisible to generic blocklists.
Step 11: Nginx Reverse Proxy and TLS
Time to put Umami behind a real domain with HTTPS. For Nginx fundamentals see How to Install Nginx on Ubuntu 24.04.
Install Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate /etc/nginx/sites-available/umami:
server { listen 80; server_name analytics.yourdomain.com;location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
proxy_read_timeout 90s; client_max_body_size 2m; } }
Enable and test:
sudo ln -s /etc/nginx/sites-available/umami /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxObtain an SSL certificate:
sudo certbot --nginx -d analytics.yourdomain.comCertbot rewrites the server block for HTTPS and auto-renews via systemd timer. Finally, tighten the firewall:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enablePort 3000 is never exposed — Nginx is the only process listening publicly.
Step 12: Backups and Updates
A daily pg_dump is all you need. Create /opt/umami/backup.sh:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR=/var/backups/umami
TS=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"docker exec umami-db pg_dump -U umami -d umami \
| gzip > "$BACKUP_DIR/umami-$TS.sql.gz"
Keep 14 days
find "$BACKUP_DIR" -name "umami-*.sql.gz" -mtime +14 -deleteSchedule it:
sudo chmod +x /opt/umami/backup.sh
( sudo crontab -l 2>/dev/null; echo "15 3 * /opt/umami/backup.sh" ) | sudo crontab -To restore:
gunzip -c umami-20260416-031500.sql.gz \
| docker exec -i umami-db psql -U umami -d umamiUpdating Umami
Umami releases every 2-4 weeks. Update with:
cd /opt/umami && docker compose pull && docker compose up -dMigrations run automatically. Take a fresh backup before major version bumps.
Using the Umami API
Umami exposes a bearer-token REST API for automation.
Generate an API key under user menu → Profile → API keys → Create API key (shown only once).
Create a website:
curl -X POST https://analytics.yourdomain.com/api/websites \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Client XYZ", "domain": "clientxyz.com"}'Read stats:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://analytics.yourdomain.com/api/websites/WEBSITE_ID/stats?startAt=1711929600000&endAt=1713139200000"Returns pageviews, visitors, sessions, bounce rate, and total time. Full reference: umami.is/docs/api.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Events not recorded | Tracking script missing on page, or hostname not in data-domains allowlist | Open browser devtools → Network, confirm script.js loads and /api/send returns 200. Remove or fix data-domains. |
| Tracker blocked by uBlock / Brave Shield | /script.js matches a generic blocklist entry | Serve from a first-party CNAME (Step 10) and rename the script path. |
| 500 error on startup | DATABASE_URL wrong, or Postgres not healthy yet | Run docker compose logs db — wait for database system is ready to accept connections. Verify user/password/host in the URL. |
| Migrations failed | Manual edits to the schema or a failed mid-upgrade | Restore from backup, or run docker compose exec umami npx prisma migrate deploy to reapply. |
| Dashboard loads but no data in realtime | App container cannot reach db service | docker compose exec umami ping -c1 db — if it fails, the compose network is broken; docker compose down && docker compose up -d. |
| 413 Request Entity Too Large | Nginx client_max_body_size too small for event payloads with many properties | Raise to 5m in the server block and reload Nginx. |
| Timezone shows UTC, want local | TZ env var not set | Add TZ: Europe/Madrid (or your zone) to the umami service environment and docker compose up -d. |
Viewing logs
docker compose logs -f umami # app
docker compose logs -f db # postgresCtrl+C to stop following.
FAQ
Umami vs. Plausible — which one should I self-host?
Umami is MIT-licensed (trivial commercial use and white-labeling) and has more built-in reports (Funnel, Retention, Journey, UTM) plus a cleaner teams model. Plausible has a more polished default dashboard, but its self-hosted Community Edition is AGPL and lags the Cloud release. For agencies and SaaS teams, Umami is usually the better fit.
Umami vs. GoatCounter — is Umami overkill?
GoatCounter is a single-binary Go project with no teams, funnels, or custom events. Perfect for personal sites. If you need funnels, event properties, teams, or to manage dozens of client sites, choose Umami.
Umami vs. Matomo — which is heavier?
Matomo is an open-source GA clone with heatmaps, session recordings, and plugins. It needs roughly 4 GB RAM and a dedicated MySQL server. Umami does ~20% of what Matomo does in ~5% of the resources. If you need session recording, use Matomo or Clarity. For numbers, funnels, and UTM — Umami wins on operational simplicity.
How long does Umami keep data?
Forever, by default. No built-in retention policy. Expect roughly 1 GB per 10M events. To enforce a window, run a monthly cron that deletes old rows from the event table and runs VACUUM FULL.
Is Umami really GDPR-compliant out of the box?
Umami stores no raw IPs, sets no cookies, and derives session IDs from a hashed combination of IP, user agent, and daily salt. This class of aggregated, non-identifying analytics does not require a cookie banner under GDPR/ePrivacy. Custom events attaching a userId or email are personal data and must be covered by your consent flow.
Can I run Umami multi-tenant?
Yes. One instance can host hundreds of websites scoped to teams. For hard isolation, run one Umami container per tenant with its own Postgres — the image is small enough to be cheap on a moderately sized VPS.
Next Steps
Recommended follow-ups:
- Connect Umami to Grafana — Postgres is readable by any BI tool.
- Add server-side events — fire
purchase-completedfrom your payment webhook. - Uptime monitoring — add an Uptime Kuma check for
/api/heartbeat. - Integrate with Next.js — the official
@umami/reactpackage provides typed hooks. - Pair with Microsoft Clarity — Umami for numbers, Clarity for recordings.
Skip the Manual Install — Get Umami Pre-Installed>
Our CloudCore Starter VPS comes with a one-click Umami image: Docker, Postgres 15, Umami v2, Nginx, Let's Encrypt, and daily backups all pre-configured.>
- Umami v2 latest, pinned and auto-updated
- PostgreSQL 15 with daily pg_dump
- Nginx + Let's Encrypt for your domain
- ufw locked to ports 22, 80, 443
- Admin password rotated and emailed to you>
Deploy Your Umami VPS Now — CloudCore Starter from EUR 7.99/month.