Back to Blog
Tutorial6 min read2026-07-13

How to Deploy a Umami Analytics Instance on PandaStack

Self-host Umami with a managed PostgreSQL database: the Docker image route, the build-from-source gotchas, migrations, and the tracking script.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Umami is one of the easiest analytics tools to self-host: a single Node.js app (built on Next.js) plus one database, and you own your data. No cookie banners, no shipping visitor data to a third party, and the dashboard is genuinely pleasant. Here's how to get your own instance running end to end, including the one build-time gotcha that trips most people up.

What Umami actually needs

Before touching any dashboard, know the shape of the thing you're deploying:

  • Runtime: Node.js 18.18 or newer.
  • Database: PostgreSQL 12+ or MySQL 8+. Umami uses Prisma as its ORM, and migrations ship with the app.
  • Configuration: one required environment variable (DATABASE_URL) and one you should always set (APP_SECRET, a random string used for hashing).
  • Port: 3000 by default, overridable with PORT.

That's it. There's no Redis requirement, no message queue, no second data store. This is why Umami is the self-hosted analytics tool I recommend to people who don't want an ops project.

Two ways to deploy: official image vs. source build

You have two realistic options, and one of them is much less work.

Option A: the official Docker image (recommended)

Umami publishes prebuilt images to GitHub Container Registry, tagged per database type. Create a new Git repo with exactly one file:

# Dockerfile
FROM ghcr.io/umami-software/umami:postgresql-latest

If you're using MySQL, swap in the mysql-latest tag. For production I'd pin a versioned tag from the [registry](https://github.com/umami-software/umami/pkgs/container/umami) instead of latest, so an upstream release never surprises you mid-redeploy.

The important property of the official image: migrations run at container startup. When the container boots, it checks the database, creates tables if they're missing, and applies any pending Prisma migrations before the app starts serving. That means the build step has zero database dependency — the database only needs to be reachable at runtime. This makes the image route the cleanest fit for any platform that injects database credentials into the running container.

Option B: building from source

You can also fork [umami-software/umami](https://github.com/umami-software/umami) and build it yourself:

git clone https://github.com/umami-software/umami.git
cd umami
npm install
npm run build
npm start

The gotcha: the build step reads DATABASE_URL. Umami inspects the connection string to decide whether to generate the Prisma client for PostgreSQL or MySQL, and if it can reach the database during the build it will apply migrations then and there. So a source build needs the database URL available at build time, not just runtime — which complicates things on platforms where secrets are injected into the running container rather than the build environment.

Unless you're patching Umami's source, use the image. You get upstream's tested build, startup migrations, and a one-line Dockerfile.

Deploying on PandaStack

The steps, in order — the ordering matters because of how the database gets wired in.

1. Provision the database. From the dashboard, create a managed PostgreSQL instance (14.x and 16.x are both fine for Umami). PandaStack runs these on Kubernetes via KubeBlocks, with daily scheduled backups retained per plan (7 days on Free, 15 on Pro, 30 on Premium).

2. Connect your repo as a container app. Point PandaStack at the repo containing your one-line Dockerfile. It builds the image with rootless BuildKit in an ephemeral Kubernetes Job pod — no host Docker daemon involved — and you can watch the build logs stream live.

3. Attach the database to the app. This is the part that saves you from credential copy-paste: once the database is attached, DATABASE_URL is injected into the container automatically. Umami reads exactly that variable, so there's nothing to translate. When the container starts, Umami's entrypoint sees the URL, connects, and runs migrations.

4. Set the remaining environment variables. In the app's environment settings:

APP_SECRET=<any long random string>

Generate one with openssl rand -base64 32. Umami uses it for hashing; if you skip it, the app falls back to deriving one, but you want this stable across redeploys so sessions and hashes don't churn.

5. Point the app at port 3000 in the service settings, then deploy.

First login — do this immediately

Umami ships with a default account:

  • Username: admin
  • Password: umami

Log in and change that password before you do anything else. An analytics instance with default credentials on a public URL is a free dashboard for anyone who finds it.

Then create your first website entry in the Umami UI (Settings → Websites → Add website). Umami gives you a website ID and the tracking snippet:

<script
  defer
  src="https://analytics.yourdomain.com/script.js"
  data-website-id="your-website-id"
></script>

Drop that into the of the site you want to track. If ad-blockers eating your data annoys you, Umami supports renaming the script via the TRACKER_SCRIPT_NAME environment variable so it isn't serving a file literally called script.js.

Custom domain and SSL

Add a custom domain like analytics.yourdomain.com in the app settings, point a CNAME at it, and SSL is provisioned automatically. Your tracking script URL is part of every page you instrument, so settle the domain *before* you paste the snippet into ten sites.

A note on the free tier

Two honest caveats if you're running this on PandaStack's free tier:

  • Scale-to-zero: idle free-tier apps scale to zero and cold-start on the next request. For an analytics collector this matters — if your tracked site gets its first visitor at 6 a.m. after a quiet night, the beacon that wakes the instance up can arrive before the app is ready, and you'll drop the first pageview or two. For a hobby site, fine. If you actually care about complete data, run it on a paid tier where the instance stays warm.
  • Database size: free-tier databases get a small storage volume. Umami's event tables grow with traffic — a personal blog will take years to notice, but a site doing serious volume should be on a bigger database from day one.

Neither of these is specific to Umami; they're the standard trade-offs of free-tier hosting, stated plainly.

Upgrading later

Because your Dockerfile pins an image tag, upgrading Umami is a one-line change: bump the tag, push, and the platform rebuilds and redeploys. Startup migrations handle the schema changes. Check the [Umami releases](https://github.com/umami-software/umami/releases) for anything flagged as breaking before major-version jumps — the [docs](https://umami.is/docs) cover migration notes when they matter.

That's the whole thing: one Dockerfile line, one attached database, two environment variables, and a password change. If you want to try it with the database wiring handled for you, spin it up at https://pandastack.io.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also