analytics · Deploy guide
Self-host Umami
Simple, fast, privacy-focused alternative to Google Analytics. Here's how to run Umami on your own infrastructure — no marketing puffery, honest pitfalls, one recommended stack.
Every guide on SwapToSaaS runs on servers we actually pay for. Umami's footprint fits comfortably on this class of instance. Different provider preference? The steps below work anywhere Docker runs.
Provision a server →Before you start
Umami is the lightest privacy-first analytics tool on this site. Node.js + Postgres. Runs happily on a €3.79/month Hetzner CX22 with 300 MB RAM to spare. This guide gets you from empty server to tracking traffic in 25 minutes.
You'll need:
- A domain (e.g.
u.yourcompany.com) pointing to your server. - Basic terminal skills.
Step 1 · Provision (5 min)
ssh root@YOUR_SERVER_IP
adduser deploy && usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
systemctl restart ssh Step 2 · DNS (5 min)
u.yourcompany.com → YOUR_SERVER_IP Step 3 · Docker (5 min)
sudo apt-get update && sudo apt-get 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
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER && newgrp docker Step 4 · Compose stack (5 min)
mkdir -p ~/umami && cd ~/umami
mkdir -p caddy_data caddy_config postgres_data ~/umami/docker-compose.yml:
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- ./postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
interval: 10s
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@postgres:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: ${APP_SECRET}
expose:
- "3000"
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy_data:/data
- ./caddy_config:/config
depends_on:
- umami
~/umami/Caddyfile:
${UMAMI_HOST} {
reverse_proxy umami:3000
} ~/umami/.env:
UMAMI_HOST=u.yourcompany.com
POSTGRES_PASSWORD=RANDOM_32_CHARS
APP_SECRET=RANDOM_32_CHARS openssl rand -base64 32 for both.
Step 5 · Bring it up (5 min)
docker compose up -d
docker compose logs -f umami Wait for ready - started server on 0.0.0.0:3000. Ctrl+C.
Open https://u.yourcompany.com. Default login is admin / umami. Change the password immediately — Settings → Profile.
Step 6 · Add your first website (2 min)
Settings → Websites → Add website. Copy the tracking snippet:
<script defer src="https://u.yourcompany.com/script.js" data-website-id="YOUR-WEBSITE-ID"></script> Paste in your site's <head>. Data appears in the dashboard within seconds of the next pageview.
Firewall and backups (5 min)
sudo ufw allow 22/tcp && sudo ufw allow 80/tcp && sudo ufw allow 443/tcp && sudo ufw --force enable Nightly Postgres backup:
sudo tee /etc/cron.daily/umami-backup > /dev/null <<'EOF'
#!/bin/bash
set -e
BACKUP_DIR=/home/deploy/backups
mkdir -p "$BACKUP_DIR"
docker exec umami-postgres-1 pg_dump -U umami umami | gzip > "$BACKUP_DIR/umami-$(date +%Y%m%d).sql.gz"
find "$BACKUP_DIR" -name "umami-*.sql.gz" -mtime +14 -delete
EOF
sudo chmod +x /etc/cron.daily/umami-backup Common pitfalls
- Ad blockers block
script.jspatterns. Some blockers match the filename. Rename it in Umami config, or serve via a randomised path via Caddy rewrite. - Default credentials. The
admin/umamidefaults must be changed before you point DNS. Anyone finding the URL can log in otherwise. - Missing timezone. Umami respects the browser's timezone but daily aggregates use the server's. Set
TZ=Europe/Berlin(or your zone) in the umami service if daily numbers look off by a day. - Reverse proxy path stripping. If you host Umami under a subpath (
/analytics/) instead of a subdomain, setBASE_PATH— otherwise assets 404.
Updating
cd ~/umami
docker compose pull
docker compose up -d
docker image prune -f What you're paying vs Umami Cloud / GA
- Umami on Hetzner CX22: €45/year (~$50) + minimal maintenance.
- Umami Cloud (free tier: 10k events/mo): $0 for small sites.
- Google Analytics: free in money, real cost in banners and DPA risk.
Umami is the ideal "run it and forget it" choice for a personal blog, small marketing site, or founder's portfolio. Lower RAM footprint than Plausible. Same privacy story.
Hosts that work well for Umami
Some links are affiliate. We only include hosts we deploy production workloads on. Rankings above are independent of these links.