GICT knowledge base
How to Self-Host Umami Analytics Using Docker on Ubuntu 24.04
Published
Umami records website visits and custom events in a database you control. A self-hosted instance needs two things: a running application and a tracking script that visitors’ browsers can reach. A working login page alone does not prove that tracking works.
This guide installs Umami with Docker and PostgreSQL on Ubuntu, puts it behind HTTPS and connects a website. It includes pageview and event checks, not a measured throughput or response-time guarantee. Use a fresh VPS or adapt the configuration without overwriting an existing website.
Private-port prerequisite: use Docker Engine 28 or newer for the loopback-published ports in this guide. Older releases have a documented localhost-port exposure limitation. Confirm from another machine that the public IP cannot reach those private service ports; a proxy login does not protect a direct bypass route.
Before you start
Use Ubuntu 24.04 with a sudo-enabled account, Docker-compatible virtualization, and a subdomain such as analytics.example.com. Point its DNS A record to the VPS. If you publish an AAAA record, it must reach the same service over IPv6. Allow inbound TCP 80 and 443 at your provider firewall; preserve SSH access.
Install Docker and prepare the project
First check that ports 80 and 443 are not already occupied:
$ sudo ss -lntp
On a fresh Ubuntu VPS, install the distribution packages:
$ sudo apt-get update
$ sudo apt-get install -y docker.io docker-compose-v2 caddy openssl curl
$ sudo systemctl enable --now docker
$ sudo systemctl stop caddy
$ sudo docker --version
$ sudo docker compose version
$ mkdir -p ~/umami
$ chmod 700 ~/umami
$ cd ~/umami
Caddy will provide HTTPS later. Leave it stopped while you replace Umami’s default login password.
Generate three different random values by running this command three times:
$ openssl rand -hex 32
Create ~/umami/.env with these contents. Replace each placeholder with a different generated value, without angle brackets or spaces:
UMAMI_VERSION=3.3.1
POSTGRES_PASSWORD=REPLACE_WITH_FIRST_RANDOM_VALUE
APP_SECRET=REPLACE_WITH_SECOND_RANDOM_VALUE
TWO_FACTOR_ENCRYPTION_KEY=REPLACE_WITH_THIRD_RANDOM_VALUE
The hexadecimal database password is safe to use inside the connection URL below. APP_SECRET protects application tokens; the separate 64-character key allows the application to support two-factor authentication. Keep both private and stable.
$ chmod 600 .env
Create ~/umami/compose.yaml:
services:
umami:
image: ghcr.io/umami-software/umami:${UMAMI_VERSION}
ports:
- "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: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/heartbeat"]
interval: 10s
timeout: 5s
retries: 12
start_period: 60s
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
TZ: UTC
volumes:
- umami-db-data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami -d umami"]
interval: 5s
timeout: 5s
retries: 12
volumes:
umami-db-data:
This follows the upstream PostgreSQL stack, with a pinned Umami release and a localhost-only application port. PostgreSQL has no public port. The named volume stores the database outside the container’s writable layer.
Start Umami and replace the default password
$ sudo docker compose config --quiet
$ sudo docker compose pull
$ sudo docker compose up -d
$ sudo docker compose ps
$ curl -fsS http://127.0.0.1:3000/api/heartbeat
A successful heartbeat returned this JSON in the test:
{"ok":true}
The first start initializes the database. Wait until both services are healthy before opening the browser. If the check fails during startup, inspect the logs and repeat it after initialization:
$ sudo docker compose logs --tail=80 umami
On your computer, open a separate terminal and start a tunnel. Replace ubuntu if your SSH account differs:
$ ssh -N -L 127.0.0.1:3000:127.0.0.1:3000 ubuntu@YOUR_VPS_IP
Keep that terminal open and visit http://localhost:3000 in your browser. Sign in with username admin and password umami. Open your account menu, select Profile, and click Change password. Save a new password immediately. Sign out, then sign in again with the new password to verify it.
Do not expose the application publicly while the default password still works. If port 3000 is occupied on your computer, use local port 3001 in the tunnel and browse to http://localhost:3001 instead.
Enable HTTPS
On the VPS, inspect Caddy’s configuration before editing it:
$ sudo cat /etc/caddy/Caddyfile
For this fresh-server setup, replace /etc/caddy/Caddyfile with the following, using your real subdomain:
analytics.example.com {
reverse_proxy 127.0.0.1:3000
}
Caddy requests and renews the certificate automatically. The hostname must resolve to this VPS, and ports 80 and 443 must be reachable. If UFW is already active, add the web rules without resetting the firewall:
$ sudo ufw status verbose
$ sudo ufw allow 80/tcp
$ sudo ufw allow 443/tcp
Do not enable a previously inactive firewall here without first allowing your actual SSH port. Validate the file and start Caddy:
$ sudo caddy validate --config /etc/caddy/Caddyfile
$ sudo systemctl enable --now caddy
$ curl -I https://analytics.example.com
Open https://analytics.example.com and confirm that you can sign in with the new password. You can now close the tunnel with Ctrl+C. Keep port 3000 bound to localhost: visitors use Caddy on port 443, not the container’s raw port.
The tracker must be publicly reachable over HTTPS. A private SSH tunnel is suitable for initial administration, but it cannot collect visits from other people’s browsers. An HTTPS website cannot load an HTTP tracking script because browsers block mixed content.
Add your website and tracking code
In Umami, open Websites and click Add website. Give it a recognizable name and enter its domain, such as www.example.com, without a protocol or path. This is the website being measured, not the analytics server’s hostname.
Open the website, click Edit, and copy its Tracking code. Add the supplied script to the website’s HTML <head>, once per page. It should resemble:
<script defer src="https://analytics.example.com/script.js"
data-website-id="YOUR_WEBSITE_ID"></script>
Use the ID from your installation. Do not copy a sample UUID from another tutorial. If your CMS provides a site-wide head-code field, use it rather than editing generated pages. Remove duplicate installations of the same tracker.
Open a page of the tracked site in a fresh browser session. In developer tools, check Network for a successful script.js load and a successful POST to /api/send. Then open that website’s analytics view in Umami, select a date range including today, and refresh it. Confirm that the visited page appears.
A pageview is not a unique visitor: reloading a page can increase pageviews without adding another visitor. Use the page path and visit time to identify your test rather than expecting both counters to increase together.
Verify a custom event
Add this button to a test page whose head already includes the tracking script:
<button type="button" data-umami-event="demo-click">Test tracking</button>
Load the page, wait for the tracking script to finish loading, and click the button once. Open the website’s Events view in Umami and look for demo-click. Refresh the view if necessary. This confirms that both page tracking and custom events reach the database.
Use a harmless test label. Do not put passwords, email addresses, or other private values in event names or properties. Remove the temporary button after checking it.
If data does not appear
- The containers are still starting. Run
sudo docker compose psand inspect the Umami logs from~/umami. A running container is not necessarily a healthy application. - The script URL uses localhost or HTTP. Copy the code while using your public HTTPS Umami address and check the actual
srcon the tracked page. - A browser extension blocked the request. Test in a clean browser profile and inspect Network errors. Browser-based analytics will not count every visit; a blocked script is not a server failure.
- The website has a Content Security Policy. Allow the analytics origin in the applicable
script-srcandconnect-srcdirectives. Merge those entries into the existing policy rather than replacing it. - The script loads but the wrong site is selected. Compare
data-website-idwith the website’s tracking code and check the dashboard date range. Loading the script alone does not prove that an event was accepted. - HTTPS is not ready. Check DNS and inbound ports, then inspect
sudo journalctl -u caddy -n 50 --no-pager. Fix the certificate problem rather than switching the tracking URL to HTTP.
Reference: Umami installation. Check the documentation for your installed version before applying changes.
Related guides
How to Self-Host DocuSeal for Document Signing
Self-host DocuSeal with private administrator setup, PostgreSQL and HTTPS. Test signing, understand email data flows and create consistent backups.
Self HostedHow to Self-Host Docmost with Docker on Ubuntu 24.04
Install Docmost with Docker, PostgreSQL and Redis. Set up a private workspace and verify editing, live updates and attachment retrieval.
Self HostedHow to Remove Image Backgrounds Locally with rembg
Remove image backgrounds locally with rembg. Preserve originals, check PNG transparency and inspect cutout quality on both light and dark backgrounds.
Self HostedHow to Translate Text Offline with Argos Translate
Translate plain text locally with Argos Translate. Install a language pair, prepare caches, verify offline operation and review translation quality.
