← Blog

Self-host Umami analytics

Railyard

May 19, 2026 · 9 min read

Follow this on your own server. Start free, no card needed

You want to know how many people visited your site today and where they came from, without sending every pageview to an ad company and without a cookie banner. Umami does exactly that: an open-source analytics dashboard that runs as one container against one Postgres database. This guide takes you from an empty server to Umami at your own domain over HTTPS, tracking your first site, with a backup you know how to restore and a plan for upgrades. The last part shows the one-click version on Railyard.

This guide uses Umami 3.4.1, the current release, with Postgres 16. Umami 3 supports Postgres only (version 12.14 or later), so there’s no MySQL option here.

What you’ll need

  • A server. Umami and Postgres are light, so a small server is plenty. A Hetzner CX23 (2 vCPU, 4 GB RAM, 40 GB NVMe) costs €5.99/month excluding VAT and leaves room for other apps. Use Ubuntu 24.04 or another recent Debian-based image.
  • A domain or subdomain you can point at the server. This guide uses analytics.example.com.
  • SSH access as root or a user with sudo.

Step 1: Point DNS at the server and update it

Create the server, note its public IPv4 address, and add an A record for analytics.example.com pointing at that address in your DNS provider. Do this first: Caddy can only get a certificate once the name resolves to your server, and DNS can take a few minutes to update.

Then log in and bring the system up to date:

ssh root@203.0.113.10
apt update && apt upgrade -y

Check the record from your own machine:

dig +short analytics.example.com

You should see your server’s IP address. If you see nothing yet, keep going and check again before Step 5.

Step 2: Install Docker

Umami, Postgres and Caddy all run as containers, so Docker Engine with the Compose plugin is the only thing to install. Docker’s convenience script sets up its official repository and packages:

curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
docker --version
docker compose version

You should see a version line for Docker and another for Docker Compose.

Step 3: Create the secrets

Make a directory for the stack and generate three values into a .env file next to it:

mkdir -p /opt/umami/backups
cd /opt/umami
echo "DB_PASSWORD=$(openssl rand -hex 24)" >> .env
echo "APP_SECRET=$(openssl rand -hex 32)" >> .env
echo "TWO_FACTOR_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
chmod 600 .env

What each one is for, from Umami’s own documentation:

  • DATABASE_URL is the only variable Umami strictly requires. You’ll build it from DB_PASSWORD in the Compose file.
  • APP_SECRET secures authentication tokens. Every installation should have its own.
  • TWO_FACTOR_ENCRYPTION_KEY is a 64-character hex string that encrypts two-factor secrets. openssl rand -hex 32 produces exactly that. Umami’s own Compose file sets it, and you need it if you plan to turn on two-factor login.

Check the file:

cat /opt/umami/.env

You should see three lines, each with a long random value.

Step 4: Write the Compose file and the Caddyfile

Save this as /opt/umami/compose.yaml. It follows the layout of Umami’s own docker-compose.yml, with pinned versions instead of latest and Caddy in front:

services:
  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    depends_on:
      - umami

  umami:
    image: ghcr.io/umami-software/umami:3.4.1
    restart: unless-stopped
    init: true
    environment:
      DATABASE_URL: postgresql://umami:${DB_PASSWORD}@db:5432/umami
      APP_SECRET: ${APP_SECRET}
      TWO_FACTOR_ENCRYPTION_KEY: ${TWO_FACTOR_ENCRYPTION_KEY}
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/api/heartbeat"]
      interval: 5s
      timeout: 5s
      retries: 5

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

volumes:
  umami-db-data:
  caddy-data:
  caddy-config:

A few things worth knowing about it:

  • Only Caddy publishes ports. Umami listens on 3000 and Postgres on 5432 inside the Compose network, so neither is reachable from the internet.
  • Compose reads ${DB_PASSWORD} and the other values from the .env file in the same directory.
  • Umami waits for Postgres to report healthy before it starts, and creates its tables on first boot.
  • The caddy-data volume keeps your certificates, so a restart doesn’t request new ones.

Now save this as /opt/umami/Caddyfile, with your own domain:

analytics.example.com {
	encode zstd gzip
	reverse_proxy umami:3000
}

Caddy reads the domain from the site address, gets a Let’s Encrypt certificate for it, redirects HTTP to HTTPS, and passes requests to the umami container. Check both files:

cd /opt/umami
docker compose config --quiet && echo "compose ok"
docker compose run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile

You should see compose ok, then Valid configuration at the end of Caddy’s output.

Step 5: Start the stack

docker compose up -d
docker compose ps

You should see db and umami both marked (healthy) after twenty or thirty seconds, and caddy as Up. Check the app from inside its container:

docker compose exec umami curl -s http://localhost:3000/api/heartbeat

You should see {"ok":true}. Then watch Caddy get the certificate:

docker compose logs -f caddy

You should see a line saying the certificate was obtained for analytics.example.com. Press Ctrl+C, and open https://analytics.example.com in your browser. You should see Umami’s login page with a valid certificate.

Step 6: Log in and change the default password

Every new Umami install creates the same account: username admin, password umami. Log in with it and change the password right away from your profile settings. Until you do, anyone who finds your login page can sign in as admin, so don’t leave a fresh install running overnight with the default.

Then add your site. Click Websites in the sidebar, then Add website, enter a name and your site’s domain, and save. Click Edit on the new website and copy the code from the Tracking code section into the <head> of your site.

Load your site in another tab. You should see the visit on the Umami dashboard within a few seconds. If you don’t, try a browser without an ad blocker: Umami’s docs note that ad blockers may stop the tracking script from loading.

Step 7: Back up the database

Everything that matters is in Postgres: your account, your websites and every pageview. Save this as /opt/umami/backup.sh:

#!/usr/bin/env bash
set -euo pipefail
cd /opt/umami
docker compose exec -T db pg_dump -U umami umami | gzip > "backups/umami-$(date +%F).sql.gz"
find backups -name 'umami-*.sql.gz' -mtime +14 -delete

Make it executable, run it once, and schedule it daily at 03:00 with crontab -e:

chmod +x /opt/umami/backup.sh
/opt/umami/backup.sh
ls -lh /opt/umami/backups
0 3 * * * /opt/umami/backup.sh

You should see a .sql.gz file of a few kilobytes on a new install, growing as data comes in. The script keeps fourteen days. Copy the files off the server too (object storage, or another machine with rsync), since a backup on the same disk doesn’t survive losing the server.

A backup only counts once you’ve restored it. To restore into a fresh install, start only the database, load the dump, then start Umami:

docker compose up -d --wait db
gunzip -c backups/umami-2026-05-19.sql.gz | docker compose exec -T db psql -q -U umami umami
docker compose up -d

Loading into an empty database before Umami starts avoids conflicts with the tables Umami creates on first boot. You should see Umami log No pending migrations to apply, and your old login and data should be back.

Step 8: Upgrade

Umami runs its database migrations when it starts, so an upgrade is a new image tag and a restart. Read the release notes on GitHub first, take a backup, then change the tag in compose.yaml (for example from 3.4.1 to the next release) and run:

/opt/umami/backup.sh
docker compose pull umami
docker compose up -d umami
docker compose logs -f umami

You should see the migrations apply, then Next.js report that it’s ready. If something goes wrong, put the old tag back and restore the backup from Step 7. Keep Caddy and Postgres current the same way: docker compose pull and docker compose up -d for Caddy, while a Postgres major upgrade (16 to 17) needs a dump and restore, not just a new tag.

The shortcut: do it with Railyard

Steps 2 through 5 are what the Railyard Umami template does for you: deploy Umami on Railyard. Pick a server, and Railyard builds Umami, creates a Postgres database for it, connects the two so there’s nothing to wire by hand, and generates APP_SECRET. The app gets an HTTPS address right away, and you can add your own domain from its Domains tab with the DNS records it shows you (Domains and HTTPS).

Step 6 still applies: log in with admin / umami and change the password immediately. Backups (Step 7) become scheduled Postgres backups with one-click restore, described in Databases and backups.

The template runs on a server in your own cloud account or one you already have, so the cost stays the server’s price. Railyard’s Free plan covers one server plus a database server and three apps, with no card. Pro is $19/month and Team is $49/month, per team.

Troubleshooting

Caddy can’t get a certificate. Nearly always DNS. Run dig +short analytics.example.com and confirm it returns your server’s IP. Also make sure ports 80 and 443 are open in your provider’s firewall, because Let’s Encrypt connects to the server to validate it. Caddy retries on its own, so once DNS is right, watch docker compose logs -f caddy.

address already in use on port 80 or 443. Something else is listening, often an Nginx or Apache installed with the image. Find it with ss -tlnp | grep -E ':(80|443)\s', stop and disable it, then run docker compose up -d again.

Umami keeps restarting. Run docker compose logs umami. The usual cause is a DATABASE_URL that doesn’t match the database. If you changed DB_PASSWORD after the first start, Postgres still has the old one: the password is only applied when the volume is created. Either put the old value back or change it inside Postgres with ALTER USER.

The dashboard shows no visits. Check that the tracking code is in the <head> of the page you loaded, that the domain on the website entry matches your site, and try a browser with no ad blocker. Your browser’s network tab should show script.js loading from your Umami domain.

The server runs out of memory. Umami and Postgres are small, but on a server shared with other apps the kernel’s out-of-memory killer can stop a container. Check with dmesg | grep -i "out of memory" and docker stats, then move something off the server or resize it.

FAQ

Does Umami set cookies? Umami is built to collect analytics without cookies, which is why many sites run it without a consent banner. Whether you need one still depends on the rules that apply to you, so check them.

Can one Umami install track several websites? Yes. Add each one under Websites and each gets its own tracking code and dashboard.

Can I run it without Docker? Yes. Umami’s docs describe a source install with Node.js 18.18 or newer, pnpm and a Postgres database, run under a process manager such as PM2. Docker is simpler to upgrade, which is why this guide uses it.

How do I move from Google Analytics? Umami starts counting from the moment the tracking code is on your site. Keep both scripts on your pages for a while if you want to compare numbers before removing Google’s.

Umami is one of the easiest self-hosted apps to run well: one container, one database and a small proxy config. Whichever way you set it up, the habits that matter are the same: change the default password on day one, and restore a backup once so you know it works.

Why people pick Railyard for this

  • Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
  • One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
  • One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
  • Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
  • Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.

Deploy Umami in one click

The Railyard template runs Umami with Postgres connected and APP_SECRET generated, on a server you own.

Free plan, no card, and 14 days of Pro for every new team. One flat price per team.