kadet-hub logokadet-hub

Umami on Kubernetes without Docker Compose, with Kapitan

Umami 3.4.1, web analytics without cookies, rendered by the kadet-hub kubernetes generator 1.14.0 from its Docker Compose file. The database is the included recipe postgresql.

What it deploys

Required parameters

applications.umami.app_secret is APP_SECRET, which signs Umami’s login sessions and the tokens it returns to the tracker. A new value logs every user out. applications.umami.two_factor_encryption_key is TWO_FACTOR_ENCRYPTION_KEY, 64 hexadecimal digits such as the output of openssl rand -hex 32. It encrypts the secrets of two-factor authentication, so keep it once a user has enabled that. Umami offers no two-factor authentication with any other value. Set both as refs with the backend of your choice. Without one of them the compile stops with Interpolation key '<key>' not found.

Include

A target includes the recipe and its options as classes and can override anything the recipe sets:

classes:
  - recipes.umami
parameters:
  namespace: umami
  kapitan:
    vars:
      target: umami
  applications:
    umami:
      app_secret: <ref>
      two_factor_encryption_key: <ref>

Fetching it

The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.

A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.1:

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/umami:1.0.1@sha256:f25b1a6ed12457c6083ae69ec4d8985600ad741286b47bf5863313a12e19dbfc
        output_path: inventory/classes/recipes/umami

Version 1.0.1 includes postgresql 1.1.1. The consumer workflow adds the included recipes at the digests the index lists. Kapitan's --fetch reads no index, so for a local compile pull them by digest first:

tmp=$(mktemp -d)
oras pull -o "$tmp" ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9
mkdir -p inventory/classes/recipes/postgresql
tar -xf "$tmp/postgresql.tar" -C inventory/classes/recipes/postgresql --no-same-owner

The first command fetches the recipe into inventory/classes/recipes/umami, the second the generators and compiles:

kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch

The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe and of the recipes it includes:

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/umami@sha256:f25b1a6ed12457c6083ae69ec4d8985600ad741286b47bf5863313a12e19dbfc \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.

Settings

Setting Inventory
Umami version components.umami.image, tag and digest
Umami’s other environment variables, such as COLLECT_API_ENDPOINT or SALT_ROTATION components.umami.env.<variable>
exposure a route or Ingress of the target to the Service umami on port 3000
resources components.umami.resources
the Cluster, its database and its size applications.postgresql, which postgresql’s README lists

The Service umami is a ClusterIP Service on port 3000. The compose file publishes that port on the host. Here an Ingress or Gateway in the target exposes it. Visitors’ browsers load the tracker /script.js and send to /api/send on the same address as the web UI.

Mapping

The service umami becomes the component umami. db becomes the included recipe postgresql, and Umami reaches it through the Service postgresql-cluster-rw in place of db.

Compose Recipe
service umami Deployment umami, Service umami on port 3000, Secret umami with APP_SECRET and TWO_FACTOR_ENCRYPTION_KEY
ghcr.io/umami-software/umami:latest ghcr.io/umami-software/umami:3.4.1@sha256:e69d1a7932e9dab0fc523edc7be1f3a53f3c261f57856fc2167ae74a8fc022e8, which latest was at migration
service db, postgres:15-alpine with the database, user and password umami and the volume umami-db-data the included recipe postgresql, PostgreSQL 18.6 on CloudNativePG, with the database and user app
DATABASE_URL: postgresql://umami:umami@db:5432/umami the key uri of the Secret postgresql-cluster-app, with the generated password and without TLS, as in the compose file (DEC-125)
APP_SECRET and TWO_FACTOR_ENCRYPTION_KEY, placeholders applications.umami.app_secret and .two_factor_encryption_key in the Secret umami
port 3000:3000 Service port 3000, ClusterIP
healthcheck curl http://localhost:3000/api/heartbeat every 5s, timeout 5s, 5 retries readiness probe with the same values
depends_on db with condition: service_healthy the init container wait-for-database of Umami’s image, which waits until the Service postgresql-cluster-rw accepts connections, because Umami’s start script exits while the database does not answer (DEC-121)
init: true not rendered. The server runs as PID 1 and ends on SIGTERM (DEC-120).
restart: always not rendered
not in the compose file UID 1001 and GID 65533 under restricted (DEC-122)

Proof

check umami --level deploys with tests/target.yml, on kind for each Kubernetes minor CI tests, with check 0.15.0 and cnpg installed first. The objects of postgresql are applied with the recipe. A compose file has no Kubernetes objects to compare with, so the recipe is proven by deploying it (RCP-38). tests/target.yml sets low-entropy test values for both parameters and runs one PostgreSQL instance.

tests/smoke requires that the pods pass Pod Security restricted and that Umami did not restart while the Cluster started. It then starts a pod of Umami’s image, which carries curl. From there, through the Service umami on port 3000, it

Not tested: the tracker script in a browser, the web UI, two-factor authentication itself, and a route or Ingress in front of the Service.

Versions

The recipe's files are in kadet-hub/recipes. All recipes