Umami: Google Analytics Alternative That Actually Respects Privacy

I got tired of GDPR banners and the ethical concerns of Google's tracking empire. Moved all my sites to Umami - a self-hosted analytics platform that gives me the insights I need without selling my users' data to advertisers. Here's what I learned deploying it.

Problem

Fresh Umami install with PostgreSQL. Container started fine, but the app showed "Database connection failed" and logging in was impossible. Checked the database - tables were created but migration seemed incomplete.

Error: Database migration failed: Relation "user" does not exist

What I Tried

Attempt 1: Recreated the database from scratch - same error appeared.
Attempt 2: Manually ran migration files - partially worked but missing indexes.
Attempt 3: Switched to MySQL - worked, but I wanted PostgreSQL for performance.

Actual Fix

The issue was that Umami needs the database to exist before the container starts, and it runs migrations on startup. I needed to: 1) Create an empty database, 2) Set the DATABASE_URL with proper SSL mode, and 3) Let the container handle migrations automatically.

# docker-compose.yml
version: '3.8'

services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://umami:your-password@db:5432/umami
      APP_SECRET: ${APP_SECRET}  # Generate with: openssl rand -base64 32
    depends_on:
      db:
        condition: service_healthy
    networks:
      - umami-network

  db:
    image: postgres:15-alpine
    container_name: umami-db
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: your-password
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    networks:
      - umami-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 5s
      timeout: 5s
      retries: 5

networks:
  umami-network:

Problem

Added the tracking script to my website. Checked Umami dashboard after 24 hours - "0 visitors". Verified the script was loading in browser console (network tab showed the request), but nothing showed up in Umami.

What I Tried

Attempt 1: Waited 48 hours thinking there was a delay - still 0 visitors.
Attempt 2: Checked Umami logs - no errors, just silence.
Attempt 3: Re-created the website in Umami dashboard - same issue.

Actual Fix

The website ID in the tracking script must match exactly. Also, if you're using a reverse proxy (Nginx/Caddy), you need to set the X-Real-IP header so Umami can correctly attribute visits. Browser ad blockers were also blocking the default path.





location /script.js {
    proxy_pass http://umami-container:3000;
    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;
}



Problem

After a month of collecting data on a medium-traffic site (~10k daily pageviews), the Umami dashboard became sluggish. Checking htop, PostgreSQL was using 80-100% CPU constantly. Loading the dashboard took 10+ seconds.

What I Tried

Attempt 1: Increased PostgreSQL shared_buffers - didn't help much.
Attempt 2: Restarted both containers - temporary fix, CPU spiked again after an hour.
Attempt 3: Deleted old data manually - broke the dashboard.

Actual Fix

The issue was that Umami was running complex analytical queries on raw event data. The solution: enable PostgreSQL extensions for better performance, add proper indexes, and set up a data retention policy to automatically aggregate/clean old data.

What I Learned

  • Lesson 1: Always use the healthcheck pattern for database dependencies - prevents race conditions during startup.
  • Lesson 2: The tracking script is sensitive to exact website ID matching - copy it directly from the Umami dashboard.
  • Lesson 3: High-traffic sites need database tuning and data retention policies - raw event tables grow indefinitely.
  • Overall: Umami is lightweight compared to GA, but not maintenance-free for production use. Plan for database growth and consider implementing data aggregation for long-running deployments.

Production Setup

Optimized production setup with PostgreSQL, reverse proxy configuration, and backups.

# docker-compose.yml (production ready)
version: '3.8'

services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami
    ports:
      - "127.0.0.1:3000:3000"  # Bind to localhost, use reverse proxy
    environment:
      DATABASE_URL: postgresql://umami:${DB_PASSWORD}@db:5432/umami
      APP_SECRET: ${APP_SECRET}
      TRACKER_SCRIPT_URL: https://analytics.yourdomain.com
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - umami-network
    volumes:
      - ./data/umami:/app/data  # For backups and config persistence

  db:
    image: postgres:15-alpine
    container_name: umami-db
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    networks:
      - umami-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  # Backup job (runs daily at 3 AM)
  backup:
    image: postgres:15-alpine
    container_name: umami-backup
    volumes:
      - ./backups:/backups
      - ./data/postgres:/var/lib/postgresql/data:ro
    networks:
      - umami-network
    command: >
      sh -c "
      while true; do
        sleep 86400 &&
        pg_dump -h db -U umami umami | gzip > /backups/umami-$$(date +%Y%m%d).sql.gz &&
        find /backups -name 'umami-*.sql.gz' -mtime +30 -delete
      done
      "
    restart: unless-stopped

networks:
  umami-network:
# .env file (generate secure values)
APP_SECRET=$(openssl rand -base64 32)
DB_PASSWORD=$(openssl rand -base64 16)

# Then: docker-compose up -d

# After setup, create admin user:
# docker exec -it umami npm run create-admin

# Default admin will be created if none exists:
# Username: admin
# Password: umami (change immediately!)

Monitoring & Debugging

Keep your Umami instance healthy with these monitoring commands:

Essential Commands

# View Umami logs
docker logs umami -f

# Check database size (monitor growth)
docker exec umami-db psql -U umami -d umami -c "
SELECT pg_size_pretty(pg_database_size('umami')) AS db_size;"

# Count total events (check data volume)
docker exec umami-db psql -U umami -d umami -c "
SELECT COUNT(*) FROM website_event;"

# Find slow queries (requires pg_stat_statements)
docker exec umami-db psql -U umami -d umami -c "
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC LIMIT 10;"

# Manual database cleanup (for retention)
docker exec umami-db psql -U umami -d umami -c "
DELETE FROM website_event WHERE created_at < NOW() - INTERVAL '90 days';"

Red Flags to Watch For

  • Database growing >100MB per month - implement data retention policy
  • Dashboard load time >3 seconds - check database performance and indexes
  • Tracking requests failing with 403 - check reverse proxy configuration
  • 0 visitors despite traffic - verify website ID and ad blocker issues

Related Resources