kadet-hub logokadet-hub

ntfy on Kubernetes: push notifications over HTTP

ntfy 2.37.2, a server that sends push notifications to phones, browsers and scripts subscribed to a topic, as a private instance with its message cache, users and attachments on a claim, designed in kadet-hub on ntfy’s image and rendered by the kadet-hub kubernetes generator 1.14.0.

What it deploys

Required parameters

applications.ntfy.base_url is NTFY_BASE_URL, the URL that phones and browsers reach ntfy at, such as https://ntfy.example.com behind an Ingress (DEC-338 in the spec). ntfy builds the links of attachments from it, and refuses to start with a path in the URL.

applications.ntfy.auth_users is NTFY_AUTH_USERS, the users of the private instance as <name>:<bcrypt hash>:<role>, several separated by commas, with the role admin or user (DEC-337). Set it as a ref. ntfy user hash of the image asks for a password and prints its hash.

Without either 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.ntfy
parameters:
  namespace: ntfy
  kapitan:
    vars:
      target: ntfy
  applications:
    ntfy:
      base_url: https://ntfy.example.com
      auth_users: <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/ntfy:1.0.0@sha256:e2af8ed9c7b9c049002b41874a95e9c5a4a860b934f9dfd475c8859f852a2bc8
        output_path: inventory/classes/recipes/ntfy

The first command fetches the recipe into inventory/classes/recipes/ntfy, 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/ntfy@sha256:e2af8ed9c7b9c049002b41874a95e9c5a4a860b934f9dfd475c8859f852a2bc8 \
  --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
ntfy version components.ntfy.image, tag and digest
access entries, tokens and other settings, such as NTFY_AUTH_ACCESS or NTFY_CACHE_DURATION components.ntfy.env.<variable>, credentials through components.ntfy.secrets with a ref
size of the claim data, 1Gi components.ntfy.volume_claims.data.spec.resources.requests.storage
exposure a route or Ingress of the target to the Service ntfy on port 80
resources components.ntfy.resources

The claim data is mounted at /var/lib/ntfy. It holds the message cache cache.db, the user database auth.db and the directory attachments. ntfy keeps messages for 12 hours and attachments for 3 hours by default. SQLite allows one replica.

NTFY_BEHIND_PROXY is true, so ntfy counts each visitor by the last address in X-Forwarded-For, which an Ingress or a Gateway adds. Without it every visitor behind the proxy would share one rate limit. A client that reaches the Service directly can set the header itself.

ntfy listens on its default port 80. The pod sets the sysctl net.ipv4.ip_unprivileged_port_start to 0, so UID 1000 binds the port without a capability (DEC-336 in the spec). Pod Security baseline and restricted allow this sysctl.

The readiness probe gets /v1/ready, the endpoint ntfy’s documentation names for load balancers. There is no liveness probe.

Proof

check ntfy --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 ntfy’s image, which carries the ntfy client and busybox wget. From there, through the Service ntfy on port 80, it

Versions

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