Analytics · Privacy · Docker

Install Umami Analytics on VPS — Privacy-First Web Analytics

Umami Analytics dashboard on VPS

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?

FeatureUmami (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

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
Tip: After the 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
Tip: Replace 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

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>
Tip: Umami's tracking script is tiny (~2KB) and loads with 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
Tip: If /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.