Install Umami Analytics on VPS — Privacy-First Web Analytics
Umami is an open-source web analytics tool focused on privacy-first — no cookies, no personal data collection, and GDPR compliant out of the box. It's an excellent alternative to Google Analytics for website owners who want reliable insights without handing data to third parties.
This guide walks you through installing Umami on a Ubuntu 22.04 VPS using Docker Compose, Nginx as a reverse proxy, and a free SSL certificate from Let's Encrypt — production-ready setup from start to finish.
Why Umami Instead of Google Analytics?
| Feature | Umami (self-hosted) | Google Analytics 4 |
|---|---|---|
| Privacy / GDPR | ✅ No personal data collected | ⚠️ Requires consent banner |
| Cookies | ✅ Cookie-free | ❌ Uses cookies |
| Data ownership | ✅ 100% yours | ❌ Google stores your data |
| Cost | ✅ Free (VPS cost only) | ✅ Free (costs your data) |
| Script size | ✅ <2KB | ❌ ~45KB+ |
| Ad blocker bypass | ✅ Self-hosted on your domain | ❌ Frequently blocked |
Requirements
- VPS running Ubuntu 20.04 / 22.04 / 24.04 (512MB RAM minimum)
- Docker + Docker Compose installed
- A domain pointing to your VPS (e.g.
analytics.yourdomain.com) - Ports 80 and 443 open in your firewall
Step 1 — Install Docker (if not already installed)
sudo apt update && sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update && sudo apt install -y docker-ce docker-compose-plugin
sudo usermod -aG docker $USER
usermod command, log out and back in for the group membership to take effect.Step 2 — Create a Docker Compose File for Umami
mkdir -p /opt/umami && cd /opt/umami
Create docker-compose.yml:
cat > docker-compose.yml <<'EOF'
version: '3'
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
restart: unless-stopped
environment:
DATABASE_URL: postgresql://umami:umami_secure_pass@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: your_random_secret_here_32chars
depends_on:
- db
ports:
- "3000:3000"
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami_secure_pass
volumes:
- umami_db:/var/lib/postgresql/data
volumes:
umami_db:
EOF
umami_secure_pass and APP_SECRET with real random values. Use openssl rand -base64 32 to generate them.Step 3 — Generate Secure Secrets
# Generate random secrets
DB_PASS=$(openssl rand -base64 24)
APP_SECRET=$(openssl rand -hex 32)
echo "DB_PASS: $DB_PASS"
echo "APP_SECRET: $APP_SECRET"
# Replace placeholder values in docker-compose.yml
sed -i "s/umami_secure_pass/$DB_PASS/g" docker-compose.yml
sed -i "s/your_random_secret_here_32chars/$APP_SECRET/" docker-compose.yml
Step 4 — Start Umami
cd /opt/umami
docker compose up -d
# Follow logs
docker compose logs -f umami
Wait about 30 seconds for Umami to initialize the database. It will then listen on port 3000.
Step 5 — Configure Nginx Reverse Proxy + SSL
sudo apt install -y nginx certbot python3-certbot-nginx
# Create Nginx config
sudo tee /etc/nginx/sites-available/umami <<'EOF'
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_cache_bypass $http_upgrade;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/umami /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# Obtain SSL certificate
sudo certbot --nginx -d analytics.yourdomain.com
Step 6 — First Login and Setup
Open your browser and navigate to https://analytics.yourdomain.com
- Default username:
admin - Default password:
umami - Change your password immediately after the first login!
Step 7 — Add a Website and Embed the Tracking Script
In the Umami dashboard go to Settings → Websites → Add website, then copy the generated tracking script and paste it into the <head> of your site:
<script defer src="https://analytics.yourdomain.com/script.js"
data-website-id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></script>
defer, so it has zero impact on your site's Core Web Vitals.Step 8 — Set Up Automated Database Backups
# Create backup script
cat > /opt/umami/backup.sh <<'EOF'
#!/bin/bash
BACKUP_DIR="/opt/umami/backups"
DATE=$(date +%Y%m%d_%H%M)
mkdir -p "$BACKUP_DIR"
docker exec umami-db-1 pg_dump -U umami umami | gzip > "$BACKUP_DIR/umami_$DATE.sql.gz"
# Retain last 14 days only
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +14 -delete
EOF
chmod +x /opt/umami/backup.sh
# Schedule nightly backup at 2:00 AM
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/umami/backup.sh") | crontab -
Verify Umami Is Working Correctly
# Check container status
docker compose -f /opt/umami/docker-compose.yml ps
# Check Umami API health
curl -s http://localhost:3000/api/heartbeat
# Expected response: {"ok":true}
# Verify database connectivity
docker compose -f /opt/umami/docker-compose.yml exec db \
psql -U umami -c '\l'
# Check for application errors
docker compose -f /opt/umami/docker-compose.yml logs --tail=50 umami
# Verify Nginx configuration
sudo nginx -t && sudo systemctl status nginx
/api/heartbeat returns {"ok":true}, Umami is ready to receive tracking events. If the tracking script isn't sending data, open your browser's DevTools → Network tab and check whether requests are reaching Umami.Common Issues and Fixes
Error: "Error: connect ECONNREFUSED 127.0.0.1:5432"
Umami can't reach the database. This usually means the db container hasn't fully started yet, or the service name in DATABASE_URL doesn't match.
docker compose ps # Check if db is running
docker compose logs db # Check PostgreSQL startup errors
# Fix: wait ~30s then restart
docker compose restart umami
Error: "invalid input syntax for type uuid"
The APP_SECRET has changed since sessions were created. Old session tokens are now invalid.
# In the browser: Ctrl+Shift+Delete to clear cookies, then log in again
# Or restart the Umami container to invalidate all sessions
docker compose restart umami
Error: "502 Bad Gateway" from Nginx
Nginx can't reach Umami — either the container is down or the port mapping is wrong.
docker compose ps # Check port mapping
curl -s http://localhost:3000 # Test direct access
sudo tail -f /var/log/nginx/error.log # Check Nginx errors
Error: "ERR_TOO_MANY_REDIRECTS" on HTTPS
A redirect loop caused by duplicate X-Forwarded-Proto headers between Nginx and Umami.
# Confirm only one X-Forwarded-Proto header is set in Nginx config
grep -n "X-Forwarded-Proto" /etc/nginx/sites-available/umami
# Should show exactly one line: proxy_set_header X-Forwarded-Proto $scheme;
Tracking Script Blocked by Ad Blockers
Self-hosted Umami scripts are rarely blocked since they run on your own domain. If blocking still occurs, rename the script endpoint:
# Add this environment variable in docker-compose.yml
TRACKER_SCRIPT_NAME: track
Frequently Asked Questions
What data does Umami collect?
Umami records the visited URL, referrer, browser, OS, device type, country (derived from IP but the IP itself is never stored), and timestamp. No IP addresses, cookies, or personal identifiers are collected, making it automatically compliant with GDPR and CCPA.
Can I track multiple websites with one Umami instance?
Yes. Umami supports multiple websites under a single account. Each site gets its own website-id, and you can share read-only dashboards with clients or team members without giving them account access.
How many resources does Umami consume?
On a 1GB RAM VPS, Umami + PostgreSQL uses roughly 200–300MB of RAM and comfortably handles tens of thousands of pageviews per day. For higher traffic volumes, increase your VPS RAM or add PostgreSQL indexes on the website_event table.