How to Self-Host Umami Analytics 2026
TL;DR
Umami is the simplest self-hosted analytics alternative to Google Analytics — no cookies, no personal data, GDPR compliant out of the box. MIT license, ~22K GitHub stars, built with Next.js and PostgreSQL. The tracking script is ~2KB. Deploy in 10 minutes with Docker Compose. If Plausible feels too complex (it needs ClickHouse), Umami is the right choice — just Postgres and a Node.js app.
Key Takeaways
- Umami: MIT license, ~22K stars, Next.js + Postgres, no cookies needed
- vs Plausible: Simpler to self-host (Postgres only, no ClickHouse), but fewer analytics features
- Script size: ~2KB vs Google Analytics' 45KB
- GDPR: No personal data collected, no cookie consent banner required
- Setup time: 10 minutes with Docker Compose
- Multi-site: One Umami instance tracks unlimited websites
Umami vs Plausible vs Google Analytics
| Feature | Umami | Plausible | Google Analytics 4 |
|---|---|---|---|
| License | MIT | AGPL 3.0 | Proprietary |
| Database | PostgreSQL | PG + ClickHouse | Cloud |
| Tracking script | ~2KB | <1KB | ~45KB |
| Cookies | None | None | Yes (consent required) |
| GDPR | ✅ Compliant | ✅ Compliant | Requires consent |
| Setup complexity | Low | Medium (ClickHouse) | N/A |
| Funnels | ❌ | ✅ | ✅ |
| Custom events | ✅ | ✅ | ✅ |
| GitHub Stars | ~22K | ~21K | — |
| Monthly cost | ~$3–6 VPS | ~$5–10 VPS | Free (data to Google) |
Choose Umami when: You want the simplest possible privacy-respecting analytics with minimal ops overhead. Choose Plausible when: You need funnels, better retention charts, and a more polished dashboard and are okay managing ClickHouse.
Part 1: Docker Compose Setup
Umami requires only two services: the app and PostgreSQL.
# docker-compose.yml
version: '3.8'
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
container_name: umami
restart: unless-stopped
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: "${APP_SECRET}" # Generate: openssl rand -hex 32
DISABLE_TELEMETRY: 1
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
volumes:
- umami_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
interval: 10s
timeout: 5s
retries: 5
volumes:
umami_db:
# .env
POSTGRES_PASSWORD=strong-database-password
APP_SECRET=$(openssl rand -hex 32)
docker compose up -d
Visit http://your-server:3000 → log in with default credentials:
- Username:
admin - Password:
umami
Change the password immediately in Settings → Profile.
Part 2: HTTPS with Caddy
analytics.yourdomain.com {
reverse_proxy localhost:3000
}
Part 3: Add Your First Website
- Settings → Websites → Add website
- Enter domain:
yourdomain.com - Name:
My Website - Copy the tracking snippet
<!-- Add to your website's <head>: -->
<script
async
defer
data-website-id="your-website-id-here"
src="https://analytics.yourdomain.com/script.js">
</script>
Since the script is served from your own domain (not umami.is), it bypasses ad blockers that target the umami.is CDN.
Next.js Integration
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html>
<head>
<Script
async
defer
data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID}
src="https://analytics.yourdomain.com/script.js"
strategy="afterInteractive"
/>
</head>
<body>{children}</body>
</html>
);
}
WordPress Integration
Install the Umami Analytics plugin:
- Plugins → Add New → search "Umami Analytics"
- Settings → Umami Analytics → enter your Website ID and script URL
Part 4: Custom Events
Track button clicks, signups, and custom interactions:
<!-- HTML attribute-based tracking (no JS required): -->
<button
data-umami-event="signup-click"
data-umami-event-plan="pro">
Sign Up for Pro
</button>
<!-- Or via JavaScript: -->
<script>
document.getElementById('cta').addEventListener('click', function() {
umami.track('cta-click', { location: 'hero', variant: 'blue' });
});
</script>
Track Page Views in SPAs
Umami auto-tracks page views on navigation. For custom SPA route changes:
// Manually trigger page view:
umami.track('/dashboard');
// With custom data:
umami.track(props => ({
...props,
url: '/checkout',
title: 'Checkout Page',
}));
Part 5: Multiple Sites and Team Access
Add Multiple Websites
One Umami instance handles unlimited sites:
- Settings → Websites → Add website for each domain
- Each site gets a unique
data-website-id - All visible from one dashboard
User Management
Create team members with specific access:
- Settings → Users → Create user
- Assign websites each user can view
- Viewer role: read-only access to assigned sites
- Admin: full access
Share Dashboard Publicly
Create a shareable link for clients or stakeholders:
- Website → Edit → Enable share URL
- Get a public read-only link:
https://analytics.yourdomain.com/share/abc123 - No login required for viewers
Part 6: Umami API
Umami exposes a stats API for embedding data in dashboards:
# Get API token:
curl -X POST https://analytics.yourdomain.com/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username": "admin", "password": "your-password"}'
# Returns: {"token": "your-jwt-token"}
# Get page views (last 24 hours):
curl "https://analytics.yourdomain.com/api/websites/{websiteId}/stats?startAt=0&endAt=86400000" \
-H "Authorization: Bearer your-jwt-token"
# Get top pages:
curl "https://analytics.yourdomain.com/api/websites/{websiteId}/metrics?type=url&startAt=0&endAt=86400000" \
-H "Authorization: Bearer your-jwt-token"
# Get active visitors (real-time):
curl "https://analytics.yourdomain.com/api/websites/{websiteId}/active" \
-H "Authorization: Bearer your-jwt-token"
Part 7: Self-Hosted vs Umami Cloud
| Self-Hosted | Umami Cloud | |
|---|---|---|
| Cost | VPS cost (~$3–6/month) | $9–$29/month |
| Setup time | 10 minutes | Instant |
| Data ownership | 100% yours | Umami's servers |
| Scaling | Manual | Automatic |
| Updates | Manual docker pull | Automatic |
| Websites | Unlimited | 3–20 |
For developers comfortable with Docker: self-host. For non-technical users or teams who don't want to maintain infrastructure: Umami Cloud at $9/month is reasonable.
Maintenance
# Update Umami:
docker compose pull
docker compose up -d
# Backup:
docker exec umami-db pg_dump -U umami umami | \
gzip > umami-backup-$(date +%Y%m%d).sql.gz
# Restore:
gunzip -c umami-backup-YYYYMMDD.sql.gz | \
docker exec -i umami-db psql -U umami -d umami
Why Self-Host Umami
Umami Cloud costs $9/month for 3 websites and $29/month for up to 20 websites. Google Analytics is free but collects personal data, requires consent banners in the EU, and sends your traffic data to Google. Self-hosted Umami eliminates the monthly fee, keeps data completely off third-party servers, and runs adequately on a $3–5/month VPS. If you're running multiple websites, the math is straightforward — even the smallest VPS costs less than Umami Cloud's single-site tier.
Compared to Plausible, Umami's architecture is dramatically simpler. Plausible requires ClickHouse — a sophisticated column-store analytics database that needs 500MB+ of RAM at idle and has more complex operational requirements. Umami stores everything in PostgreSQL (or MySQL), which you almost certainly already have running for other services. Sharing a PostgreSQL instance between Umami and another app is trivial and saves you running an additional database container. This simplicity makes Umami the right first analytics tool for developers new to self-hosting.
The 2KB tracking script is genuinely unobtrusive. Google Analytics loads ~45KB of JavaScript, which blocks rendering and contributes to worse Core Web Vitals scores. Umami's script loads asynchronously, has no impact on page rendering, and doesn't create any first-party or third-party cookies. In the EU, this means no consent banner is required for most legal frameworks — eliminating the cookie consent wall that costs many sites meaningful engagement.
The multi-site capability with no additional cost is a strong argument for agencies and developers. You add a website in Settings, copy the snippet, paste it in — done. No additional billing, no per-site limits, no need to create separate accounts. One Umami instance can serve analytics for every project you maintain.
When NOT to self-host Umami. Umami lacks some features that Plausible has: no funnels, no more detailed acquisition reporting, and the dashboard is less polished. If you need conversion funnels or detailed session-level analysis, Plausible is worth the additional ClickHouse operational complexity. For enterprise-level analytics with heatmaps, session recordings, and funnel analysis, you're looking at dedicated tools like PostHog.
Prerequisites
Umami is two services: the Next.js application and PostgreSQL. Together they use around 200–300MB RAM at idle, making it one of the most resource-efficient analytics stacks available. A shared Hetzner CX11 (2 vCPU, 2GB RAM) at €3.29/month works fine for a low-traffic site. A Hetzner CX22 (4GB RAM) at €4.50/month is comfortable for multiple high-traffic websites with concurrent dashboard users. See the VPS comparison guide for provider comparisons.
Docker Engine 24+ and Docker Compose v2 are required. The APP_SECRET must be generated before first run — use openssl rand -hex 32. This value signs authentication tokens; changing it after first launch will invalidate all active sessions but won't lose any analytics data.
DNS: create an A record for analytics.yourdomain.com pointing to your server. Port 80 and 443 must be open for Caddy's certificate provisioning. Once DNS propagates, run docker compose up -d and Caddy will automatically obtain a Let's Encrypt certificate.
If you're running PostgreSQL for another application already, you can skip the db service in the compose file and point DATABASE_URL at your existing PostgreSQL instance. Create a dedicated umami database and user first.
Production Security Hardening
Umami's attack surface is relatively small — it's a Next.js app with a PostgreSQL backend — but the host server still needs hardening.
UFW firewall. Block all ports except SSH, HTTP, and HTTPS:
ufw default deny incoming
ufw default allow outgoing
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Umami's internal port 3000 should not be publicly accessible — only Caddy should reach it via localhost.
Change the default admin password immediately. The default credentials (admin / umami) are publicly documented. Change them in Settings → Profile within seconds of first login.
Fail2ban. Protect the login form and SSH:
apt install fail2ban -y
Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
systemctl restart fail2ban
Secrets management. Keep APP_SECRET and POSTGRES_PASSWORD in .env with restricted permissions:
chmod 600 .env
SSH hardening. Use key-based authentication only:
# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no
systemctl restart sshd
Automatic OS updates:
apt install unattended-upgrades -y
dpkg-reconfigure --priority=low unattended-upgrades
Database backups. Analytics data lives in PostgreSQL. Even if individual page view records aren't critical, historical trend data is valuable. Schedule nightly backups with Restic — the automated backup guide covers encrypted off-server backups in detail. A pg_dump of Umami typically compresses to a few hundred MB even for busy sites.
For a complete security hardening reference, see the self-hosting security checklist.
Troubleshooting Common Issues
"Invalid username or password" with default credentials. If you're sure you haven't changed the password yet, the database may not have initialized correctly. Check whether the db container is healthy: docker compose ps. If the db service shows unhealthy, check its logs: docker compose logs db. A common cause is insufficient disk space — PostgreSQL fails to initialize if there's not enough room to write initial data files.
Tracking script not recording page views. Open your browser's developer tools → Network tab and look for requests to /api/send. If you see a 404, the script tag URL or data-website-id may be wrong. If you see a CORS error, your Umami domain is likely different from what's expected. If there are no requests at all, verify the script tag is in the <head> and not being blocked by a browser extension. Test in a clean browser profile without extensions.
Dashboard shows no data for a newly added website. After adding a website in Settings, a new data-website-id is generated. You must update your script tag with the new ID. The old script tag from a previous website won't work for a new site entry. Copy the snippet from the website settings page.
Database connection errors in Umami logs. Check docker compose logs umami for the specific error. "Connection refused" means PostgreSQL isn't running or isn't accepting connections yet. "Authentication failed" means the DATABASE_URL credentials don't match what PostgreSQL was initialized with. If you recently changed the POSTGRES_PASSWORD in .env, you also need to update it inside the running PostgreSQL container or recreate the database volume.
High memory usage after running for days. Next.js applications can develop memory leaks in long-running processes. If memory climbs steadily, set up a scheduled restart: add restart: unless-stopped to the Umami service and periodically trigger a graceful restart with docker compose restart umami. This typically happens only under high load.
Shared dashboard links not working. If you've enabled share URLs for a website (Edit → Enable share URL), ensure Caddy is correctly forwarding the /share/* path to Umami. The share URL uses the same base domain as your analytics instance. If users get a 404 on the share link, the Caddy reverse proxy may have a misconfigured path matcher.
Data not visible after time zone change. Umami stores timestamps in UTC internally. The dashboard displays data in the browser's local time zone. If you change server time zone settings or move between regions, existing data remains intact — it's always stored in UTC. If you see unexpected date shifts, check your browser's time zone and compare it against your expected reporting period.
Slow dashboard loading with large datasets. Umami's PostgreSQL-backed analytics is fast for typical small-to-medium sites, but very high-traffic sites with tens of millions of events can see slow dashboard queries. Add a PostgreSQL index on the session table's created_at column if you experience this. Alternatively, Umami Cloud is a reasonable option for very high-traffic sites where database management becomes a burden.
Tracking blocked by browser extensions despite self-hosting. Most ad blockers target umami.is CDN URLs, not self-hosted instances. However, some blockers (uBlock Origin with certain filter lists) block the request based on the data-website-id pattern rather than the domain. To maximize data accuracy, consider naming your tracking script something generic (e.g., /analytics/collect) using a Caddy rewrite rule that maps to your Umami endpoint. This removes the recognizable pattern that some filter lists target.
Multiple visits counted as unique for the same user. Umami uses a combination of IP hash and user agent to identify unique visitors without cookies. If your site is served behind a CDN that normalizes IP addresses (Cloudflare, for example), many visitors may appear as coming from the same IP, which can cause anomalies in unique visitor counts. This is a known limitation of cookieless analytics — it's the privacy-preserving tradeoff. Umami's documentation discusses this in detail for sites with CDN traffic.
See all open source Google Analytics alternatives at OSSAlt.com/alternatives/google-analytics.
See open source alternatives to Umami on OSSAlt.