Skip to main content

The deploy pipeline

A deploy runs as a queued job from a single trigger to a running, routed revision. The trigger is either a webhook or a poll; both resolve to the same job and produce the identical build.

The flow

git push

▼ webhook (POST /api/deploy/<host>) or poller (git ls-remote)
▼ DeployFromPush (queue: incus; one attempt; long timeout)

├─ build kixctl-build --flake git+<clone>?rev=<sha> --attr <a> --kind container
│ → a NixOS image (metadata + rootfs)
├─ config the app's stored configuration → credential files
├─ import the image over the Incus API, aliased <app>-<sha7> (idempotent)
└─ launch the immutable instance on a target member, on the owned network

▼ publish, or land alongside → reachable by name

The build is pinned to the exact commit — git+<clone_url>?rev=<sha> — so Nix fetches the repository hermetically at that revision, with no working clone and no "whatever the branch is right now" ambiguity. Import is idempotent and aliased by the revision name, so re-running the same commit is a no-op rather than a duplicate.

Not safe to retry blindly

DeployFromPush runs with a single attempt, on purpose. A deploy is not something to auto-retry blindly — every failure path (build failed, bad build output, no cluster, import failed, launch failed) logs and returns rather than throwing into a retry loop. A failed build is surfaced on the Updates tab; you retry it deliberately with Deploy now.

Webhook authentication

The trigger endpoint is stateless — no session, no CSRF. The signature is the authentication, not a logged-in user. The host signs the raw request body with HMAC-SHA256, and kixctl verifies it in constant time against the repository's own secret. The raw body must be read as received, because re-encoding the parsed payload would change bytes and break the signature. Only pushes to the tracked branch deploy; a push to any other branch is acknowledged and ignored.

If you run Forgejo, its webhook allow-list blocks private IPs by default. Scope it to your cluster's subnet so the webhook can reach kixctl — it is an anti-SSRF guard, not a TLS setting, though it first reads like one.

Registering repositories and choosing webhook versus poll is covered in Deploy an app.