Self-Hosting Umami Analytics on a VPS: Our Actual Docker Setup (2026)

We track eleven products from one self-hosted Umami install at zero extra cost. The Docker Compose setup, port pinning, Nginx reverse proxy, and what we learned the hard way.

We evaluate every tool based on published features, real-world usage, community feedback, and independent testing where possible. Affiliate commissions never influence our rankings. How we research ยท Editorial policy

What this article is and is not

We wrote a separate guide that walks through Fathom, Plausible, Umami, and PostHog - what each tool is, how the pricing compares, and which to pick in which situation. This is not that article.

This is the setup article for people who have already decided on Umami and want to know exactly how to run it on a VPS with Docker. The Docker Compose configuration, the port pinning, the Nginx reverse proxy, and the things that bite you if you skip them.

We run Umami at analytics.ragingorangutan.com and use it to track eleven products from a single install. If that is the setup you are trying to replicate, this is what we did.

What you need before you start

A VPS with Docker installed. We run on Hostinger CloudPanel with Docker Compose. Umami is genuinely lightweight - 512MB RAM is the documented minimum for Umami plus PostgreSQL. If your VPS is already running other services, you want more headroom, but as a dedicated install 512MB holds.

A subdomain to serve it from. We use a subdomain of our main holdings domain. Any subdomain you control works - analytics.yourdomain.com is the obvious choice.

DNS access to point that subdomain at your VPS IP.

About twenty minutes for the initial setup. The Nginx configuration is the part that takes longest if you have not done a reverse proxy before.

The Docker Compose setup

Umami runs as two containers: the application and a PostgreSQL database. Start with the official Docker Compose file from the Umami GitHub repository rather than writing your own - it handles the healthcheck dependency between containers correctly. Then make one critical change before you run it.

The change is the port mapping. The default file maps port 3000 on the container to port 3000 on the host. Change the host port to something specific to Umami - a port in the high 3000s or 4000s that nothing else on your server uses. The format is host-port:container-port, so you are changing only the left side. The container port stays 3000.

This matters because Nginx will point at a specific port. If Docker ever assigns a different host port on restart, your analytics subdomain breaks silently. Pin it to a dedicated port and document it somewhere.

The APP_SECRET environment variable needs to be a long random string. Generate one with openssl rand -base64 32 in your terminal. This is what Umami uses to sign session tokens - do not leave it as the placeholder value.

Set restart: always on both containers. This brings Umami back up automatically after a server reboot or Docker restart. Without it, you do maintenance, the server reboots, and you come back to a broken dashboard.

The three required changes before first run: pin the host port, set a real APP_SECRET, set restart: always on both containers.

The Nginx reverse proxy

Once the containers are running, Nginx forwards requests from your subdomain to the Umami container. The minimal config is a server block that listens on port 443 with SSL, matches your analytics subdomain, and proxies requests to localhost on your pinned port.

The proxy headers you need: proxy_set_header Host, proxy_set_header X-Real-IP, and proxy_set_header X-Forwarded-For. Without these, all visits appear to come from your own server IP and your country and device analytics show only localhost.

If you are on Hostinger CloudPanel, it handles SSL certificate provisioning in the panel - add the subdomain, point it at the Docker service port, and CloudPanel handles the Let's Encrypt certificate. If you manage Nginx manually, certbot --nginx is the standard approach.

Before reloading Nginx, always run nginx -t first. A config syntax error can bring down every site proxied through that server, not just the one you were editing.

First login: the three things to do immediately

The default credentials are admin / umami. Change them before you do anything else. Umami's admin panel is publicly accessible on your subdomain - default credentials are a known attack surface.

Then add your first website. In Settings, add a Website entry with the domain you want to track. Umami generates a website ID - this ties the tracking script on your frontend to the data in the dashboard.

Copy the tracking script. It is a single script tag with two attributes: the src pointing at your Umami instance's /script.js, and data-website-id set to the ID from the previous step. Paste this into your app's HTML head element, ideally with a defer attribute. That is the entire frontend integration.

Tracking multiple products from one install

This is the main reason we chose self-hosting over Fathom or Plausible cloud for a portfolio. Cloud plans typically charge per pageview volume and sometimes per site. One Umami install handles unlimited sites from a single dashboard at no additional cost.

For each product, add a Website entry in Umami Settings. Each gets its own website ID. Put the corresponding tracking script in each product's HTML head. One dashboard, separate datasets per product, no additional billing.

The practical upshot: you log into your analytics subdomain once and switch between all your products in the left nav. No managing multiple accounts, no per-product invoicing, no cross-contaminated data. Each product is its own clean dataset in the same place.

The port pinning lesson

This warrants its own section because the failure mode is invisible until it costs you. Docker containers without pinned host ports - written as bare "3000" in docker-compose.yml rather than "3033:3000" - will have Docker assign whatever free port it finds on restart.

Nginx is pointing at a specific port number. If the container comes back on a different port, Nginx returns a 502 error and your analytics subdomain is broken. The failure looks like an application error. It takes time to realise it is actually a port mismatch.

The fix is one character change in docker-compose.yml. Pin the port, write it down, and apply the same rule to every Docker service on the VPS. This is not Umami-specific - it is the correct Docker discipline on any server where Nginx needs to find a container reliably.

What you get and what you give up

Umami gives you pageviews, sessions, bounce rate, visit duration, referrers, entry pages, countries, devices, and operating systems. It is privacy-first by design - no cookies, no PII stored, no consent banner required under most interpretations of GDPR. Clean and fast.

What it does not have: heatmaps, session replays, funnel analysis, A/B testing, or user-level event tracking. If you need to understand where users drop off in a signup flow or which features they actually use, Umami will not tell you that. It is traffic analytics, not product analytics. PostHog is the tool for the product analytics case.

The other trade is operational responsibility. If your VPS goes down, your analytics tracking stops. Fathom and Plausible run on infrastructure they maintain with uptime SLAs. You are on the hook for your own server. For most solo founders this means the occasional gap during a planned restart - acceptable. For a business where real-time traffic data is critical, the managed services earn their fee.

Updates and maintenance

Umami ships updates as new Docker image versions. The update process is: pull the latest image, stop the containers, restart them. The database schema migrations run automatically on startup.

In practice: docker compose pull in your Umami directory, then docker compose down and docker compose up -d. Check the Umami releases page on GitHub before pulling - occasionally a release has a known issue worth waiting one cycle on.

The PostgreSQL container does not need updating as frequently as the application. When you do update Postgres, back up the database first: docker exec your-db-container pg_dump -U umami umami > umami-backup.sql. Run that command to a file stored off-server. Losing the Postgres volume would lose all historical analytics data.

Automate the pg_dump backup if the analytics data matters to you. A daily cron job dumping to object storage costs almost nothing and removes the "I meant to do that" failure mode.

Bottom Line
Free to run, one setup, all your products

Self-hosting Umami makes sense if you already have a VPS and you are tracking more than one or two products. One install, no per-site fees, privacy-first by default. The trade is that you own the running of it - updates, backups, uptime. For most solo founders managing a product portfolio on a VPS they already pay for, that trade is clearly worth it. If you are starting from zero and Docker is new territory, pay for Fathom or Plausible until the infrastructure is established. Come back to self-hosting when the server is already running and the marginal cost of adding one more service is low.

Frequently Asked Questions

512MB RAM is the documented minimum for Umami plus PostgreSQL. If your VPS already runs other services - apps, Nginx, PM2 processes - you want at least 1-2GB to avoid memory contention. Umami itself is lightweight; PostgreSQL is the more resource-hungry part. On a VPS already running several Node apps, Umami adds modest overhead.

Umami does not use cookies and does not store personally identifiable information. It uses a session hash derived from anonymised visitor data. The official documentation states no cookie consent banner is required. Hosting your data in an EU data centre reduces cross-border transfer complexity for EU visitors. Verify the current guidance applies to your specific situation - the Umami documentation covers their privacy model in detail.

Yes. Add a Website entry in Umami Settings for each domain you want to track. Each gets its own website ID and tracking script. Switch between sites from the dashboard left nav. The self-hosted version has no limit on the number of sites.

The data lives in your PostgreSQL container volume. The simplest backup: docker exec your-db-container pg_dump -U umami umami > backup.sql. Store the output file off the VPS - in object storage, an S3 bucket, or even a scheduled copy to a second machine. If the VPS goes down taking the volume with it, that backup file is your recovery path.

Some links on this site earn us a commission at no cost to you. We only recommend tools we have used ourselves. Rankings are never influenced by commission rates.