You can host your own analytics platform on a $6 VPS in less time than it takes to drink a coffee. Umami runs on a single Docker container plus a database, ships with a 2KB tracking script, never sets cookies, and keeps every byte of behavioural data on a server you control. No third-party data processor agreement, no consent banner, no monthly bill. That last one matters more than people admit. Once you self-host, the dashboard is yours forever and there is nothing standing between you and your visitor logs.
This guide walks through the actual mechanism: what Umami is, what your server needs, the docker-compose file that gets you running, the nginx block that puts it behind a real domain with HTTPS, and the five things that go wrong on first install. By the end you will have a working analytics endpoint at analytics.yourdomain.com, an admin login, a tracking script in your site’s <head>, and a backup plan. If you are still weighing whether self-hosting is the right call, see our roundup of GA alternatives for the broader landscape.
What Umami Gives You
Umami is an open-source web analytics tool licensed under MIT, written in Node.js with a Next.js front-end. The project lives at github.com/umami-software/umami and ships a single Docker image. What you actually get when you stand it up:
- Cookieless tracking. Visitor identity is reconstructed from a daily-rotating hash of IP plus user-agent plus a salt, scoped to the website ID. No persistent identifier is written to the browser, which means no cookie banner is required under GDPR for analytics purposes.
- 2KB tracking script. The injected JavaScript is roughly 2 kilobytes gzipped. By comparison the GA4 gtag.js loader is around 90KB before it loads its child bundles. Page-load impact is negligible even on a 3G mobile connection.
- Modern dashboard. Real-time visitor view, sessions, page views, referrers, UTM breakdowns, country and device reports, custom events, and a SQL-style filter UI. The interface is responsive and works on a phone.
- Multi-site, multi-user. A single Umami install can track hundreds of websites under different team scopes. You can give clients read-only access to their own dashboards.
- Custom events with properties. Fire
umami.track('signup', { plan: 'pro' })from any client-side JavaScript and the event lands in your dashboard with searchable properties. - Public share URLs. Generate an unguessable share link for any website’s dashboard so stakeholders can view metrics without an account.
- API access. Read endpoints for every metric the UI shows, plus a server-side
sendendpoint for first-party event ingestion. Useful if you want to proxy events through your own backend.
What Umami does not give you: server-side rendering of marketing attribution, advanced funnel analysis with cohort retention, or session recordings. Umami is a focused page-and-event tracker. If you need heatmaps or replays, pair it with a separate tool. If you need full product analytics, look at PostHog instead. The full feature scorecard sits on our dedicated Umami Analytics tool page alongside the other privacy-first vendors we track.
Self-Host vs Umami Cloud: Two Paths
Umami offers a hosted SaaS at cloud.umami.is. Pricing starts at $0 for 10K events/month and scales linearly. The hosted version uses the same codebase as the open-source release, so the dashboard you see is identical. The decision tree is straightforward:
Pick Umami Cloud if: you do not want to manage a server, your traffic is under 100K events/month and the free tier covers you, your DPO is comfortable with a US-based processor (Umami Cloud runs on AWS us-east-1), or you want one-click upgrades when new versions ship.
Self-host if: you want every byte of analytics data inside your own infrastructure (a hard requirement for some EU healthcare and financial verticals), you are running multiple sites and the per-event pricing on Cloud would exceed $20-30/month, you already have a VPS with spare capacity, or you want to extend Umami with custom code.
The practical rule we use across our portfolio: self-host as soon as you have more than two sites, because a $6 VPS will track all of them. For a single low-traffic site, Cloud’s free tier is genuinely free and saves you the maintenance overhead.
Server Requirements
Umami is light. The Node.js process idles at around 80MB resident memory and the database is the limiting factor at scale. A dirt-cheap VPS handles substantial traffic.
| Traffic tier | Recommended specs | Monthly VPS cost (Hetzner/DO) |
|---|---|---|
| Up to 100K page views/mo | 1 vCPU, 1GB RAM, 25GB SSD | $4-6 |
| 100K – 1M page views/mo | 2 vCPU, 2GB RAM, 40GB SSD | $8-12 |
| 1M – 10M page views/mo | 4 vCPU, 4GB RAM, 80GB SSD, separate DB | $25-40 |
| 10M+ page views/mo | 4 vCPU, 8GB RAM + managed PostgreSQL | $60+ |
Operating system: any Linux distribution with Docker support. We test on Debian 12 and Ubuntu 22.04 LTS. Both work identically.
Database choice: Umami supports both PostgreSQL 12+ and MySQL 8+. Use PostgreSQL unless you have a strong reason not to. PostgreSQL handles the analytics workload better — Umami’s schema uses several jsonb columns for event properties and PostgreSQL’s jsonb operators are mature. MySQL works fine but the JSON path queries are slower under load.
Network: outbound HTTPS only for image pulls and version checks. Inbound 80/443 for the dashboard and tracking endpoint. No exotic ports.
Quick Start: Docker Compose Setup (10 minutes)
Assuming you have a fresh VPS with Docker and docker-compose installed, here is the entire stack. Save the file below as docker-compose.yml in /opt/umami/ on your server.
# /opt/umami/docker-compose.yml
# Umami v2.x with PostgreSQL 16
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000" # bind to localhost only; nginx proxies in front
environment:
DATABASE_URL: postgresql://umami:CHANGE_ME_LONG_PASSWORD@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: CHANGE_ME_64_HEX_CHARS_FROM_OPENSSL_RAND
DISABLE_TELEMETRY: "1" # opt out of anonymous version-check pings
TRACKER_SCRIPT_NAME: "script.js" # rename to bypass ad blockers (see below)
depends_on:
db:
condition: service_healthy
networks:
- umami-net
db:
image: postgres:16-alpine
container_name: umami-db
restart: unless-stopped
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: CHANGE_ME_LONG_PASSWORD
volumes:
- ./postgres-data:/var/lib/postgresql/data
- ./backups:/backups
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
interval: 10s
timeout: 5s
retries: 5
networks:
- umami-net
networks:
umami-net:
driver: bridge
Three things to change before you bring this up:
- Replace both occurrences of
CHANGE_ME_LONG_PASSWORDwith the same long random string. Generate one withopenssl rand -base64 32. - Replace
CHANGE_ME_64_HEX_CHARS_FROM_OPENSSL_RANDwith the output ofopenssl rand -hex 32. This is the secret used to sign session cookies. - Decide whether to keep the
TRACKER_SCRIPT_NAME: "script.js"override. Default isumami.js, which is on every ad blocker filter list. Renaming defeats blockers but check our discussion on consent and tracking if you are sensitive to user expectations.
Now bring it up:
cd /opt/umami
docker compose up -d
docker compose logs -f umami
You should see a line in the logs that reads “ready in Xms” once the database migrations complete and the Next.js server starts. First boot takes 30-60 seconds because Umami runs Prisma migrations against the empty database. From then on, restarts are instant.
At this point Umami is listening on 127.0.0.1:3000. It is not yet reachable from the internet. That’s the next step.
Connecting a Database
The compose file above bundles PostgreSQL as a sibling container. That is fine for sites under 1M events/month and is what most self-hosters run. If you outgrow it, swap the DATABASE_URL environment variable to point at a managed PostgreSQL instance:
DATABASE_URL: postgresql://umami:[email protected]:5432/umami?sslmode=require
Then remove the db service and the depends_on block from compose. Umami runs its migrations automatically on startup; no schema bootstrapping is needed on the managed side beyond creating an empty database and a user with full privileges on it.
If you prefer MySQL, change the image tag to ghcr.io/umami-software/umami:mysql-latest, set DATABASE_TYPE: mysql, and use a MySQL-style URL: mysql://umami:password@db:3306/umami. The schema is functionally identical but stored differently.
Reverse Proxy with Nginx
Umami should not be exposed directly. You want HTTPS, you want to control headers, and you want flexibility to add rate-limiting later. Here is a working nginx server block for a host like analytics.yourdomain.com:
# /etc/nginx/sites-available/umami
server {
listen 80;
server_name analytics.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name analytics.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/analytics.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/analytics.yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Logging - keep IPs anonymised at the source
access_log /var/log/nginx/umami-access.log;
error_log /var/log/nginx/umami-error.log;
client_max_body_size 5M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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 X-Forwarded-Host $host;
proxy_read_timeout 60s;
}
}
Symlink it into sites-enabled, run nginx -t to validate, then issue a Let’s Encrypt certificate:
ln -s /etc/nginx/sites-available/umami /etc/nginx/sites-enabled/
nginx -t
certbot --nginx -d analytics.yourdomain.com --redirect --agree-tos -m [email protected]
systemctl reload nginx
Certbot’s nginx plugin rewrites the SSL paths automatically. After this step, hitting https://analytics.yourdomain.com in a browser shows the Umami login screen.
First Login + Dashboard Tour
The default credentials are admin / umami. Change them immediately. Click the user icon in the top right, choose Profile, then Change password. Pick something long. This account has full admin rights to every site and team in your install, so treat it like a root credential.
Adding a website is two clicks. Settings > Websites > Add website. Enter a display name and the domain. Umami generates a unique website ID (a UUID) which you embed in the tracking script. You can edit settings later, change the timezone, or set a custom share URL slug.
The dashboard for each site shows seven default panels: Views, Visitors, Bounce rate, Average session, Pages, Referrers, and Browsers/OS/Devices. Click any data point to filter the entire view by that segment. The URL captures the filter state, which means you can bookmark a “last 7 days, mobile only, organic referrer” view and come back to it.
Adding the Tracking Script
Once a website is registered, Umami shows you the snippet under Settings > Websites > Edit > Tracking code. It looks like this:
<script async defer
src="https://analytics.yourdomain.com/script.js"
data-website-id="abc12345-6789-4def-0123-456789abcdef"></script>
Drop it in the <head> of every page you want tracked. WordPress users can paste it into a header hook in their theme’s functions.php, or use a header-snippets plugin. Static-site users add it to the layout template.
If you use Google Tag Manager, create a Custom HTML tag, paste the snippet, and trigger it on All Pages. Umami works fine through GTM. The script is small and synchronous-loading does not block render because of the async defer attributes.
Custom events fire from any client-side JavaScript:
// Fire a simple event
umami.track('newsletter-signup');
// Fire an event with properties
umami.track('purchase', { product: 'pro-plan', value: 49 });
// Track a virtual page view (for SPAs)
umami.track(props => ({ ...props, url: '/checkout/step-2' }));
Events appear under the Events tab in the dashboard within seconds. There is no client-side buffering or batching by default, which means data is real-time but every event is a network round-trip. For high-volume sites you can wrap the tracker in your own batching layer if needed.
Routine Maintenance
Backups. The only state worth backing up is the PostgreSQL database. A nightly pg_dump is sufficient. Add this to root’s crontab on the host:
0 3 * * * cd /opt/umami && docker compose exec -T db pg_dump -U umami umami | gzip > ./backups/umami-$(date +\%Y\%m\%d).sql.gz
0 4 * * 0 find /opt/umami/backups/ -name 'umami-*.sql.gz' -mtime +14 -delete
That keeps 14 days of dumps locally. For an offsite copy, sync the backups/ directory to S3 or Backblaze B2 with rclone in the same cron.
Version upgrades. Umami releases minor versions roughly monthly. The upgrade procedure is two commands:
cd /opt/umami
docker compose pull
docker compose up -d
Migrations run automatically on container start. Check the release notes on GitHub before pulling a major version (v1 to v2 was a schema break) but minor versions inside the v2 line are safe.
Retention. Umami keeps everything forever by default. For GDPR-conscious teams, set up a retention policy. The simplest approach is a SQL job that purges raw events older than 26 months while keeping aggregated daily rollups. Umami does not ship this out of the box but the schema is documented and the queries are five lines.
Resource monitoring. Watch docker stats occasionally. If the database container’s memory creeps above 80% of host RAM, you have outgrown the bundled PostgreSQL and should move to a managed instance. Comparable considerations apply to Matomo’s self-hosted deployments but with higher baseline resource needs — our Matomo compared with Umami piece quantifies the resource gap.
Common Setup Issues
| Symptom | Likely cause | Fix |
|---|---|---|
| Umami container restarts in a loop, logs show “Can’t reach database server” | Database password mismatch between Umami env and Postgres env, or Postgres still initialising | Confirm both POSTGRES_PASSWORD and the password inside DATABASE_URL are identical. Add the healthcheck shown above so Umami waits for the DB. |
| Tracking script returns 200 but no data shows in dashboard | Wrong data-website-id attribute, or the domain on the website settings does not match the page domain |
Open browser devtools > Network, find the /api/send request, check the JSON payload. The website field must match the UUID shown in Umami settings. |
| CORS error in browser console: “Access-Control-Allow-Origin” | Reverse proxy stripping headers, or Umami served from a domain not whitelisted in CLIENT_IP_HEADER |
Confirm nginx is forwarding X-Forwarded-Host and X-Forwarded-Proto. Umami trusts these by default. No CORS config needed unless you are calling the API from a separate origin. |
| Cookie domain warning in dashboard | You set COOKIE_DOMAIN env var to the wrong scope (e.g. .example.com when serving from analytics.example.com) |
Remove the COOKIE_DOMAIN override unless you specifically need to share session cookies across subdomains. Default behaviour is correct for 99% of installs. |
| Some visitors not tracked, especially Firefox and Brave users | Ad blockers block umami.js by default; uBlock Origin’s filter list catches it |
Set TRACKER_SCRIPT_NAME: "script.js" in compose and update your <script src> URL. Also rename the API endpoint with COLLECT_API_ENDPOINT if you want full evasion. |
None of these are deal-breakers. The first three account for 80% of failed installs we have seen and all resolve in under five minutes once you know where to look.
Umami vs Plausible Self-Hosted
Plausible is the obvious comparable. Both are MIT-licensed, cookieless, lightweight, and aimed at the same demographic. The differences are real but subtle.
| Dimension | Umami | Plausible |
|---|---|---|
| Language / runtime | Node.js (Next.js) | Elixir (Phoenix) |
| Database | PostgreSQL or MySQL | PostgreSQL + ClickHouse (required) |
| Memory footprint at idle | ~150MB total stack | ~400MB total stack (ClickHouse is hungry) |
| Self-host docs quality | Single docker-compose example, works first try | Official self-host repo, requires both PostgreSQL and ClickHouse |
| Multi-site handling | Unlimited sites in one install, no licence | Self-hosted is unlimited but the feature lag behind Cloud is more pronounced |
| Custom events | Free-form, with arbitrary properties | Limited to 30 named goals plus revenue tracking |
| UI polish | Functional, less designed | More polished and opinionated |
| Funnel and goal analysis | Basic — relies on filters and segments | Built-in funnel UI |
Our take: Umami wins on simplicity of self-host (one database, smaller footprint, single image) and on event flexibility. Plausible wins on UI polish and built-in marketing features. If you are choosing between them for a small portfolio, see our deep-dive on Plausible and the dedicated Plausible vs Umami head-to-head for the other side. For most self-hosters with multiple low-traffic sites, Umami is the lower-friction choice.
When NOT to Self-Host
Self-hosting is not free in time. The actual costs are:
- Initial setup: 30-60 minutes. If you have never used Docker, double it.
- Monthly maintenance: 10-15 minutes. Pull updates, verify backups, glance at logs.
- Incident response: hours, occasionally. If your VPS has an outage, your analytics goes down with it. Cloud has a dedicated SRE team; you do not.
Take Cloud if any of the following apply: you do not have a VPS already, your traffic comfortably fits the free tier (under 10K events/month), you are uncomfortable with Linux command-line operations, or your team has zero appetite for owning another piece of infrastructure. The hosted version is honestly cheap relative to engineering hours.
Self-host if any of these apply: you are running multiple sites and the per-event Cloud pricing crosses $20/month, you have a hard data-residency requirement (EU healthcare, financial services, government contracts), you already manage other Docker stacks on a VPS, or you want to extend Umami with custom features. For a broader view of how analytics fits into a privacy-friendly setup, our complete privacy-analytics guide covers the legal and design considerations.
Frequently Asked Questions
What is the minimum monthly cost to self-host Umami?
Around $4-6/month for a 1 vCPU, 1GB VPS at Hetzner or DigitalOcean, plus a $10-15/year domain. That single VPS will handle several hundred thousand page views per month across multiple sites. We have one running at $5.83/month tracking 14 sites with 280K monthly page views combined.
Can Umami scale to 1 million visits per month?
Yes, on a 2 vCPU / 2GB VPS with bundled PostgreSQL. At 5-10 million visits you should move PostgreSQL to a managed instance with at least 2GB of dedicated RAM and tune shared_buffers to 25% of that. Umami itself remains stateless and can run on a small node.
Should I use MySQL or PostgreSQL?
PostgreSQL. Umami’s analytical queries lean heavily on jsonb for event properties and PostgreSQL handles those better than MySQL’s JSON type. The performance gap is small at low traffic but widens past 500K events/month. The compose file above defaults to PostgreSQL for this reason.
Does Umami work with Google Tag Manager?
Yes. Add the Umami snippet as a Custom HTML tag in GTM and trigger on All Pages. Custom events also work — wrap each umami.track() call in its own Custom HTML tag triggered by the relevant GTM event. There is no measurable performance penalty from the indirection.
What is the syntax for custom events?
umami.track('event-name') for simple events, or umami.track('event-name', { key: 'value', plan: 'pro' }) for events with properties. Properties can be strings, numbers, or booleans. Nested objects are flattened. Events are visible in the dashboard within 30 seconds.
Is Umami GDPR proof out of the box?
For analytics purposes, yes. No cookies are set, no persistent identifier is written to the browser, IPs are hashed before storage. You are still the controller and need a privacy policy mentioning Umami, but you do not need a consent banner for analytics under the cookie-banner test set out by EU DPAs. If you also fire marketing scripts that set cookies, you still need consent for those — see our practical small-business analytics guide for how to scope this correctly.
Can I import historical data from GA4?
Not directly. Umami has no GA4 importer. The pragmatic approach is to install Umami in parallel, let it accumulate forward-looking data, and treat the GA4 archive as a frozen historical reference. If you need pre-2026 data inside Umami, you would have to write an ETL that reads BigQuery exports and posts to Umami’s /api/send endpoint, which is several days of engineering for diminishing returns. Most teams just keep the GA4 console as a reference and stop looking at it after 90 days.
Bottom Line
Self-hosting Umami is a 15-minute job once you have a VPS and a domain. The stack is minimal: one Docker image for the app, one for the database, one nginx config, one Let’s Encrypt cert. Maintenance is roughly ten minutes a month. The recurring cost is a $5 VPS bill, not a per-event subscription, which means your unit economics get better as you grow rather than worse.
The reasons people stay on hosted analytics are usually inertia and risk-aversion, not actual operational complexity. If you can run a WordPress site on a VPS, you can run Umami on the same box. If you have multiple sites and the maths works out, the right move is to self-host today and be done with monthly analytics bills forever.
For a wider view of where Umami sits among the alternatives we test, see our comparison of 15 GA alternatives. The short version: Umami is the best self-host option for cookieless tracking when you want to keep complexity low and own your data outright.