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