Gotify on Kubernetes: a self-hosted push notification server
gotify 3.1.1, a server that takes messages from applications through a REST API and delivers them to its web UI and Android app, with its SQLite database on a claim, designed in kadet-hub on gotify’s image and rendered by the kadet-hub kubernetes generator 1.14.0.
- Latest version:
1.0.0, published - Source: container image
docker.io/gotify/server:3.1.1@sha256:44fc5bbd1c0618878e33073d38c2c13126b48f245c3fe12236b1da48eb203b86, recipe designed in kadet-hub - Proof level:
deploys, deployed on kind and smoke tested by the kadet-hub/recipes CI renderspassed in the release run of with check 0.20.2, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade F 71 findings, one per advisory and package, in the 1 image of version 1.0.0: CRITICAL 2, HIGH 33, MEDIUM 29, LOW 5, UNKNOWN 2. Scanned 2026-11-14 12:09 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:docker.io/gotify/server.
What it deploys
recipes.gotify(init.yml)recipes.gotify.gotify(gotify.yml):gotify
Required parameters
applications.gotify.admin_password is GOTIFY_DEFAULTUSER_PASS, the
password of the admin admin that gotify creates at a start while its
database has no user (DEC-344 in the spec). Set it as a ref.
gotify reads the password only when it creates the admin, so a changed parameter does not change the password of an existing admin. Change it in the web UI.
Without the parameter the compile stops with Interpolation key and its
name.
Include
A target includes the recipe and its options as classes and can override anything the recipe sets:
classes:
- recipes.gotify
parameters:
namespace: gotify
kapitan:
vars:
target: gotify
applications:
gotify:
admin_password: <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.0:
parameters:
kapitan:
dependencies:
- type: oci
source: ghcr.io/kadet-hub/recipes/gotify:1.0.0@sha256:4a43d46c4c3fac327a4d77e8636453d6ca2d6483133416f86c1d628503f0a785
output_path: inventory/classes/recipes/gotify
The first command fetches the recipe into inventory/classes/recipes/gotify, 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:
gh attestation verify \
oci://ghcr.io/kadet-hub/recipes/gotify@sha256:4a43d46c4c3fac327a4d77e8636453d6ca2d6483133416f86c1d628503f0a785 \
--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 |
|---|---|
| gotify version | components.gotify.image, tag and digest |
the variables of gotify-server.env.example, such as GOTIFY_DEFAULTUSER_NAME or GOTIFY_SERVER_TRUSTEDPROXIES |
components.gotify.env.<variable>, credentials through components.gotify.secrets with a ref |
size of the claim data, 1Gi |
components.gotify.volume_claims.data.spec.resources.requests.storage |
| exposure | a route or Ingress of the target to the Service gotify on port 80 |
| resources | components.gotify.resources |
The claim data is mounted at /app/data, the directory that the
installation page mounts. It holds the SQLite database gotify.db, the
uploaded application images in images and the directory plugins, from
which gotify loads plugins at its start. SQLite allows one replica.
gotify listens on its default port 80. The pod sets the sysctl
net.ipv4.ip_unprivileged_port_start to 0, so UID 1234 binds the port
without a capability (DEC-343 in the spec). Pod Security baseline and
restricted allow this sysctl.
The readiness probe gets /health, which the HEALTHCHECK of gotify’s
Dockerfile asks. There is no liveness probe.
Proof
check gotify --level deploys with tests/target.yml, on kind for each
Kubernetes minor CI tests. The image has no Kubernetes objects to compare
with, so the recipe is proven by deploying it (RCP-44).
tests/smoke checks that the pod passes Pod Security restricted and
starts a pod of gotify’s image, which carries curl. From there, through
the cluster IP of the Service gotify on port 80, it
- gets
/health - gets 401 from
/messagewithout credentials and with a wrong password, and for a message pushed with a wrong application token - creates an application as the admin of
tests/target.yml, pushes a message with the application’s token and lists it as the admin - deletes the pod
gotify-0and, once its successor is ready, lists the message again as the admin.
Versions
1.0.0, :sha256:4a43d46c4c3fac327a4d77e8636453d6ca2d6483133416f86c1d628503f0a785
The recipe's files are in kadet-hub/recipes. All recipes