S SwapToSaaS

analytics · Deploy guide

Self-host Umami

By Fig, Editor Last revisited Back to Google Analytics alternatives

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.

Difficulty
★★★★
Trivial
Time to first login
25 min
Realistic, not marketing
Server cost
€3.79/mo
2 vCPU · 4 GB RAM · 40 GB SSD
License
MIT
37.5k stars
Recommended host
Hetzner CX22 2 vCPU · 4 GB RAM · 40 GB SSD · €3.79/mo

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.js patterns. Some blockers match the filename. Rename it in Umami config, or serve via a randomised path via Caddy rewrite.
  • Default credentials. The admin/umami defaults 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, set BASE_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.

Related deploy guides