What you will set up
Umami counts the visits to your websites: pages viewed, where visitors came from, their country, browser and device, without cookies. You add one script tag to your site, and look at the numbers in Umami's dashboard. The visitor data stays on your server.
Here it runs as a pod under a user of its own called umami, behind Caddy from the Podman guide: Umami itself, and PostgreSQL for its data.
Umami is developed by Umami Software, Inc., a company registered in Delaware, United States, and is open source under the MIT licence. By default, the dashboard makes your browser load an image from i.umami.is with Umami's version number, and ask api.umami.is for the newest version; both answer from Cloudflare's network. The dashboard also loads the icons of the sites it lists from DuckDuckGo's icon service. And each time Umami starts, the database tool it uses, Prisma, reports its version, the operating system, the Node.js version and a hash of the install path to checkpoint.prisma.io, on servers in the United States. Step 3 switches all four off. The visitors to your site only ever contact your own server.
Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:
- Umami's built-in account
adminwith the passwordumamiwas changed over an SSH tunnel before Umami could be reached from the internet; from outside, the old password was then refused. Two-factor authentication worked. - A page on another site, served by the same Caddy, was visited in a browser, and the visits showed up in Umami with the visitor's real country and city.
- A visitor who sent a made-up address in an
X-Real-IPheader was counted under a false country until step 3'sCLIENT_IP_HEADERline was added, and under the real one after it. - With the settings in step 3, the dashboard made no requests to Umami's servers or to DuckDuckGo, and Umami's start made no DNS lookups at all.
- A dump of the database was restored,
podman auto-updateupdated Umami 3.3.1 to 3.4.0, and everything came back by itself after a reboot.
Umami used about 300 MB of memory, nearly all of it Umami itself; PostgreSQL used about 20 MB. Umami keeps everything in PostgreSQL. Plausible also runs ClickHouse, a database built for large numbers of events, and used about 600 MB in its guide, with countries but no regions or cities unless you add a MaxMind account; Umami shows cities out of the box.
Before you start
You need:
- a server set up as in the Podman guide, with Caddy running, and secured with ufw;
- an A record and an AAAA record for
stats.example.compointing at your server; - a website to track, here
www.example.com.
The examples use 203.0.113.10 for your server's address. Replace the names and the address throughout.
1. Create the user
As root:
useradd -m -s /bin/bash umami
loginctl enable-linger umami
machinectl shell umami@
Everything up to step 4 runs as umami.
2. Create the secrets
pw=$(openssl rand -hex 24)
printf %s "$pw" | podman secret create umami-db-password -
printf 'postgresql://umami:%s@127.0.0.1:5432/umami' "$pw" | podman secret create umami-database-url -
unset pw
openssl rand -hex 32 | tr -d '\n' | podman secret create umami-app-secret -
openssl rand -hex 32 | tr -d '\n' | podman secret create umami-2fa-key -
The first two hold the database password, once for PostgreSQL and once in the address Umami connects to. The third signs Umami's logins, and the fourth protects two-factor authentication.
3. Describe the pod
mkdir -p ~/.config/containers/systemd
cd ~/.config/containers/systemd
Create umami.pod:
[Pod]
PodName=umami
# Only Caddy, on this server, can reach Umami: the port is not open to the internet.
PublishPort=127.0.0.1:8112:3000
[Install]
WantedBy=default.target
Create umami-db.container:
[Container]
ContainerName=umami-db
Image=docker.io/library/postgres:17-alpine
Pod=umami.pod
Volume=umami-db:/var/lib/postgresql/data
Environment=POSTGRES_USER=umami POSTGRES_DB=umami
Secret=umami-db-password,type=env,target=POSTGRES_PASSWORD
AutoUpdate=registry
[Service]
Restart=always
And umami-app.container:
[Unit]
After=umami-db.service
[Container]
ContainerName=umami-app
Image=ghcr.io/umami-software/umami:3
Pod=umami.pod
Secret=umami-database-url,type=env,target=DATABASE_URL
Secret=umami-app-secret,type=env,target=APP_SECRET
Secret=umami-2fa-key,type=env,target=TWO_FACTOR_ENCRYPTION_KEY
Environment=CLIENT_IP_HEADER=x-forwarded-for
Environment=TRACKER_SCRIPT_NAME=insights.js
Environment=DISABLE_TELEMETRY=1 DISABLE_UPDATES=1 CHECKPOINT_DISABLE=1
Environment=FAVICON_URL=https://{{domain}}/favicon.ico
AutoUpdate=registry
[Service]
Restart=always
What the last lines do:
CLIENT_IP_HEADERmakes Umami take the visitor's address fromX-Forwarded-For, which Caddy always sets itself. Without it, Umami first looks at headers such asX-Real-IP, which Caddy passes on from the visitor unchanged, so anyone could pick the country they are counted under. Umami uses the address to look up the country and city, in a database built into the image, and to tell visitors apart; it does not store it.TRACKER_SCRIPT_NAMEserves the tracking script under a name of your choice as well asscript.js, and the tracking code in step 6 then uses it. Step 6 explains what it does against ad blockers, and what it does not.TWO_FACTOR_ENCRYPTION_KEY, from the fourth secret, protects the two-factor secrets in the database. Without it, Umami does not offer two-factor authentication.DISABLE_TELEMETRYandDISABLE_UPDATESstop the dashboard's image fromi.umami.isand its version check atapi.umami.is. Without the version check, you are not told about new versions, but step 8 installs them anyway.CHECKPOINT_DISABLEstops Prisma's report at each start.FAVICON_URLmakes the dashboard load each site's icon from the site itself instead of from DuckDuckGo.
The 3 tag follows every release of Umami 3, and Umami updates its database by itself when it starts. PostgreSQL stays on version 17: a new major version of PostgreSQL needs a dump and a restore.
Start it:
systemctl --user daemon-reload
systemctl --user start umami-pod
systemctl --user enable --now podman-auto-update.timer
The first start downloads the images, about 1.4 GB on disk, and takes a minute or two. curl -s http://127.0.0.1:8112/api/heartbeat prints {"ok":true} once Umami is ready.
4. Change the default password
Umami creates an account admin with the password umami, which anyone can look up. Change it before Umami can be reached from the internet. From your own computer, open an SSH tunnel to the server:
ssh -L 8112:127.0.0.1:8112 root@203.0.113.10
Then open http://localhost:8112 in your browser. That goes straight to Umami, not through Caddy. Log in with Username admin and Password umami. Choose your user name, admin, at the bottom left, then Settings, Profile and Change password. Enter umami as Current password, a long password of your own as New password and Confirm password, and choose Save.
Then turn on two-factor authentication: under Security in the same settings, switch on Enable 2FA, scan the QR code with an authenticator app, and enter the 6-digit code it shows. From then on, Umami asks for a code after your password. Close the tunnel with exit.
5. Put Caddy in front
As root, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile:
stats.example.com {
reverse_proxy 127.0.0.1:8112
}
Restart Caddy with systemctl --user restart caddy. Then check, from any computer, that the old password no longer works:
curl -s -X POST https://stats.example.com/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"username": "admin", "password": "umami"}'
The answer should contain "code":"incorrect-username-password".
6. Track your website
Open https://stats.example.com, log in, and in Websites choose Add website. Enter a Name and the site's Domain, www.example.com, and choose Save. Then choose the edit icon on the site's row. Under Tracking code is the tag to add to the <head> of every page of your site:
<script defer src="https://stats.example.com/insights.js" data-website-id="2e75e9c8-6f8e-4fa6-b3fd-627a235d5a6f"></script>
Your Website ID is different. Once the tag is on your site, open a page in your browser: the visit shows up under Realtime and Overview within seconds, with your country and city under Location.
Ad blockers stop the script by its address. EasyPrivacy, a list most blockers use, blocks script.js on any host whose name starts with umami., and AdGuard's tracking list blocks every request to such a host, which is why this guide uses stats.example.com. On that name, none of four common lists (EasyList, EasyPrivacy, AdGuard Tracking Protection and uBlock Origin's privacy list) blocked the script or the visits it sends, under either script name, so TRACKER_SCRIPT_NAME only helps against a list that adds a rule for the default name, and a blocker that adds a rule for your own host still stops both.
7. Back up
As umami:
mkdir -p ~/backup
podman exec umami-db pg_dump -U umami umami > ~/backup/umami.sql
podman secret inspect --showsecret --format '{{.SecretData}}' umami-2fa-key | tr -d '\n' > ~/backup/umami-2fa-key
That saves your account, your websites and all their statistics, and the key without which the two-factor secrets in the dump cannot be read. Copy ~/backup to another machine and keep it private, or let restic take the dump every night (its step 6).
To restore, as umami:
systemctl --user stop umami-app
podman exec umami-db dropdb -U umami --force umami
podman exec umami-db createdb -U umami umami
podman exec -i umami-db psql -q -U umami umami < ~/backup/umami.sql
systemctl --user start umami-app
8. Updates
Once a day, podman-auto-update.timer fetches newer images for Umami and PostgreSQL and restarts the pod with them. To update right away, as umami:
podman auto-update
podman image prune -f
The first line lists both images with true under UPDATED when there was something new. The second removes the old images, which take about 1 GB each.
Troubleshooting
Your visits do not show up, and /api/send answers {"beep":"boop"}. Umami ignores visits from bots, and counts headless browsers and curl as bots. Test with an ordinary browser.
Visits from the server itself show no country. Umami ignores addresses that belong to the machine it runs on, and inside the pod the server's own addresses are the pod's. Visits from other machines are not affected.
The script is blocked in your own browser. Your ad blocker has a rule for Umami's host or script name: see step 6.