How to Deploy Umami on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host Umami, the cookieless web analytics app, with Docker Compose and PostgreSQL. Covers generated secrets, the default login you must change, and the one script tag.
- A small VPS: 1 vCPU / 1–2 GB RAM
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain (or subdomain) for the analytics dashboard
- Docker Engine + Compose installed (see the base guide below)
What Umami is
Umami is privacy-focused web analytics under the MIT
licence. It counts page views, visitors, referrers, devices and locations
without cookies. The free build also includes funnels, user journeys,
retention, goals, session replays and heatmaps. You add one <script> tag to
your site and read the results in Umami's dashboard.
The app is Next.js. PostgreSQL is its only supported database, at v12.14 or newer. The official Docker Compose file runs two containers, the app and Postgres, which makes it one of the lighter analytics stacks to self-host.
If you are comparing options: Plausible vs Umami covers the closest alternative, Matomo vs Umami covers the heavyweight Google Analytics replacement, and Deploy Plausible on a VPS is the matching guide for Plausible.
Server sizing
The catalog lists 1 GB RAM as the minimum. On our test box (GCP e2-standard-2, Ubuntu 26.04, Docker 29.8.1), the idle stack measured about 165 MB of RAM and about 1.8 GB of disk for the images and an empty database.
- 1 vCPU / 1 GB RAM is enough for a handful of small sites.
- 2 GB RAM gives PostgreSQL room once you track busier sites or keep long history. Every page view is a row.
- Disk grows with traffic. Start with 20–40 GB and watch the database volume.
Run it on its own small box, or next to other light services. Analytics traffic comes from your visitors' browsers, so the server has to be reachable from the public internet over HTTPS.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed, along
with a non-root user and a ufw firewall. If not, start with
Docker & Compose on Ubuntu.
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
Paid link — we earn a commission if you shop through it.
Install Umami (Docker Compose)
The upstream compose file ships with placeholder secrets: APP_SECRET,
TWO_FACTOR_ENCRYPTION_KEY and a database password of umami. Below is the
same file with those values moved into a generated .env, and the port bound
to loopback so the reverse proxy is the only public way in.
Two of the secrets have rules in Umami's docs:
APP_SECRETsecures authentication tokens. Each installation should have its own unique value.TWO_FACTOR_ENCRYPTION_KEYmust be a 64-character hex string, andopenssl rand -hex 32produces exactly that. Without it, nobody can enable 2FA. Changing or losing it later makes the stored 2FA secrets unreadable.
mkdir -p ~/umami && cd ~/umami
[ -f .env ] || printf 'POSTGRES_PASSWORD=%s\nAPP_SECRET=%s\nTWO_FACTOR_ENCRYPTION_KEY=%s\n' \
"$(openssl rand -hex 24)" "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > .env
chmod 600 .env
cd ~/umami
cat > docker-compose.yml <<'YAML'
services:
umami:
image: ghcr.io/umami-software/umami:latest
ports:
# Loopback only: Caddy is the only way in from outside.
- "127.0.0.1:3000:3000"
environment:
DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami
APP_SECRET: ${APP_SECRET}
TWO_FACTOR_ENCRYPTION_KEY: ${TWO_FACTOR_ENCRYPTION_KEY}
depends_on:
db:
condition: service_healthy
init: true
restart: always
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
interval: 5s
timeout: 5s
retries: 5
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- umami-db-data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
volumes:
umami-db-data:
YAML
Start it. On first boot Umami creates its tables and a default admin account:
cd ~/umami
docker compose up -d
timeout 300 bash -c 'until curl -fsS -o /dev/null http://127.0.0.1:3000/api/heartbeat; do sleep 3; done'
docker compose ps
HTTPS + domain
Point an A record such as stats.example.com at the server, wait for it to
resolve, then put Caddy in front of the loopback port, following
Automatic HTTPS with Caddy:
stats.example.com {
reverse_proxy 127.0.0.1:3000
}
HTTPS matters more here than for most apps. Your tracked sites are served over HTTPS, and browsers block a tracker script loaded over plain HTTP from an HTTPS page.
If Umami sits behind a proxy or CDN that puts the visitor's address in a
non-standard header, set CLIENT_IP_HEADER to that header name. Otherwise
every visitor's location resolves to the proxy's location. Umami's docs have a
separate page for Cloudflare's headers.
First login: change the default password
Open https://stats.example.com. Umami's docs give the default credentials as
username admin, password umami, with a warning to change the password
immediately after the first login. Do that before anything else. Until you
do, anyone who finds the URL can log in.
Then:
- Turn on two-factor authentication: click your profile in the side nav,
then Settings, then Security. This works because you set
TWO_FACTOR_ENCRYPTION_KEY. Admins can also require 2FA for everyone. - Create a separate login for each person instead of sharing
admin. Umami has users and teams for this.
Add a site and install the tracker
In the dashboard, go to Websites → Add website. Enter a name, and the site's real domain in the Domain field. Umami uses that domain to keep your own site out of the referrer list.
Open the website's Edit screen and copy the snippet from the Tracking
code section into the <head> of every page. It looks like this:
<script defer src="https://stats.example.com/script.js" data-website-id="…"></script>
Visit your site and the visit shows up in the dashboard right away. On a
Next.js site, add the tag with next/script rather than a plain <script>.
Single-page apps need no extra setup, because Umami tracks client-side
navigation automatically.
Ad blockers. Some block script.js on analytics hosts. Umami supports
renaming the script with the TRACKER_SCRIPT_NAME environment variable. Add it
to the compose environment block and recreate the container.
Backups
Everything is in PostgreSQL, plus the .env that holds its password and the
app secrets. Dump the database from inside its container:
cd ~/umami
mkdir -p ~/backups/umami
docker compose exec -T db pg_dump -U umami umami | gzip > ~/backups/umami/umami-$(date +%F).sql.gz
cp .env ~/backups/umami/env-$(date +%F)
ls -lh ~/backups/umami
Copy the folder off the box, and keep the .env copy somewhere private.
Losing TWO_FACTOR_ENCRYPTION_KEY forces every 2FA user to enrol again.
Restoring means starting a fresh db container and piping the dump into
psql -U umami umami.
Upgrades
Upstream's Docker instructions are to pull the new image and restart:
cd ~/umami
docker compose pull
docker compose up -d
Back up first. Umami runs schema migrations when it starts. After a major
upgrade, the docs recommend running ANALYZE; in PostgreSQL to refresh the
query planner's statistics, because stale statistics can make the dashboard
slow on large instances. It is safe on a live database:
cd ~/umami
docker compose exec -T db psql -U umami -d umami -c 'ANALYZE;'
Troubleshooting
No data appears. Open your site with the browser's developer tools on the
Network tab and look for the request to your Umami host. If the script fails
to load, check that the src URL is HTTPS with a valid certificate, and try
from a browser without an ad blocker.
Every visitor shows the same country. Umami is reading the proxy's IP
address. Set CLIENT_IP_HEADER to the header your proxy or CDN uses.
The app restarts in a loop right after install. Read docker compose logs umami. A DATABASE_URL whose password doesn't match the one Postgres was
initialised with is the usual cause. That happens when .env is regenerated
after the first start. The [ -f .env ] || guard above prevents it.
Two-factor can't be enabled. TWO_FACTOR_ENCRYPTION_KEY is missing or is
not 64 hex characters. Fix it in .env and run
docker compose up -d.
Verification + next steps
You're done when https://stats.example.com loads over a valid certificate,
the admin/umami login no longer works, your own account has 2FA on, a test
visit to a tracked site appears in the dashboard, and an off-box pg_dump has
been restored at least once.
From there: add goals and funnels for the pages that matter, and give each site's owner their own team. If you outgrow Umami's feature set, Matomo vs Umami and Deploy Matomo on a VPS cover the heavier option.