Kapitan quickstart: Uptime Kuma on Kubernetes with kind
From an empty directory to Uptime Kuma, a self-hosted monitoring tool, on a kind cluster on your machine: install Kapitan, fetch the uptime-kuma recipe pinned by version and digest, verify it, compile it and apply the manifests. The steps take about five minutes, most of it waiting for the image and the first readiness probe.
What you need
Docker, kind, kubectl, uv and the GitHub CLI gh, logged in with gh auth login for the verify step.
1. Create a kind cluster
kind runs a Kubernetes node as a container and points kubectl at it.
kind create cluster --name quickstart-uptime-kuma
2. Install Kapitan
This installs Kapitan 0.36.3 as a uv tool with the pins of the consumer docs. omegaconf stays at 2.4.0.dev4 because later releases removed ListMergeMode, which Kapitan imports, and oras lets Kapitan fetch OCI artifacts. Python 3.13 is pinned because Kapitan requires regex below 2025, which has no wheels for newer Pythons, so uv would build it from source. uv puts kapitan into ~/.local/bin.
uv tool install --python 3.13 --with omegaconf==2.4.0.dev4 --with oras 'kapitan[omegaconf,reclass-rs]==0.36.3'
3. Write the project
This creates the project directory and selects the omegaconf inventory backend:
mkdir -p uptime-kuma/fetch/targets uptime-kuma/inventory/targets
cd uptime-kuma
cat > .kapitan <<'EOF'
global:
inventory-backend: omegaconf
EOF
The fetch inventory pins the recipe by version 1.0.1 and digest. It is a separate inventory because Kapitan reads the inventory before it fetches, so a class cannot be fetched by the inventory that includes it:
cat > fetch/targets/recipes.yml <<'EOF'
parameters:
kapitan:
dependencies:
- type: oci
source: ghcr.io/kadet-hub/recipes/uptime-kuma:1.0.1@sha256:dfa63d2f7c6eb147ca0ea28d8f5d5f0f1bb2a5cba4fac9ba5bbe3fbc8d7a51d8
output_path: inventory/classes/recipes/uptime-kuma
EOF
The target includes the recipe as a class and sets the namespace and the target's name, which is also its output directory under compiled/:
cat > inventory/targets/uptime-kuma.yml <<'EOF'
classes:
- recipes.uptime-kuma
parameters:
namespace: uptime-kuma
kapitan:
vars:
target: uptime-kuma
EOF
4. Verify the recipe
Kapitan's --fetch checks neither the digest nor the attestation. This checks that kadet-hub's release workflow built the recipe at this digest and signed its provenance. On a terminal gh prints what it verified; in a script only the exit status tells.
gh attestation verify \
oci://ghcr.io/kadet-hub/recipes/uptime-kuma@sha256:dfa63d2f7c6eb147ca0ea28d8f5d5f0f1bb2a5cba4fac9ba5bbe3fbc8d7a51d8 \
--owner kadet-hub \
--cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
It does not check that the index still lists the digest, nor the generator the recipe pins. Verifying a recipe by hand does both and pulls exactly the verified digest.
5. Compile
The first command fetches the recipe into inventory/classes/recipes/uptime-kuma and then exits with status 1 and No such file or directory: .../compiled, because the fetch target compiles nothing. The second fetches the kubernetes generator the recipe pins into lib/kubernetes and writes a StatefulSet and a Service to compiled/uptime-kuma/manifests/. It warns that the container sets no resource requests; a target sets them under components.uptime-kuma.resources.
kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch
6. Apply
The recipe renders no Namespace, so the first command creates it. rollout status waits until the pod is ready. Its readiness probe runs every 60 seconds, so this takes a minute or two after the image is pulled.
kubectl create namespace uptime-kuma
kubectl apply -R -f compiled/uptime-kuma
kubectl -n uptime-kuma rollout status statefulset/uptime-kuma --timeout=5m
7. Open Uptime Kuma
This forwards port 3001 on your machine to the Service until you stop it with Ctrl+C. Open http://localhost:3001. Uptime Kuma asks for an admin account on the first visit, without a login, and anyone who reaches the Service can claim it, so create the account before you expose the Service.
kubectl -n uptime-kuma port-forward service/uptime-kuma 3001:3001
Clean up
This deletes the cluster and everything in it.
kind delete cluster --name quickstart-uptime-kuma
Next
The recipe page lists what a target can override. Every recipe page has the same fetch inventory and commands for its recipe, and the consumer workflow runs the verification and the compile in CI.
Coming from Helm?
The recipe takes the place of the chart, and the target takes the place
of values.yaml. What you would pass with --set goes under parameters
in inventory/targets/uptime-kuma.yml, and the target can override any
value the recipe’s classes set, not only the ones a template exposes.
kapitan compile takes the place of helm template: it writes manifests,
and nothing keeps a release in the cluster.
Writing a Helm chart vs Kapitan rebuilds metrics-server, cert-manager and kube-prometheus-stack as one inventory and compares the objects on kind. Helm vs Kustomize vs Kapitan on one Kubernetes app renders the same app with all three tools for two environments. For a Bitnami chart, Bitnami chart alternatives names the replacement and the recipe where there is one.
Coming from Kustomize?
The recipe’s classes play the part of a base, and the target plays the
part of an overlay: the inventory merges the classes in order, and the
target’s parameters override them.
Helm vs Kustomize vs Kapitan on one Kubernetes app
compares overlays for two environments with the same app as an inventory.
A field that neither the recipe nor a chart sets is changed after
rendering, with a patch in Kustomize and with the
mutate generator in Kapitan. Three posts make the
same change both ways on rendered charts:
adding fields with a JSON patch,
setting an environment variable with valueFrom
and deleting a resource.