zook

These are the invariants that every Zook-managed stack must satisfy. The whole auto-rollback design rests on them. Violate one and the guarantees break.

The contract applies to both runtimes (docker and native). Runtime-specific sections are marked accordingly.

1. Immutable artifacts — never latest or mutable references

Docker stacks: Every service image must be tagged with an immutable version string — a semver tag (v1.2.3), a git SHA, or any other identifier that never moves.

Do not use latest or any other mutable tag. Rollback works by bringing the previous version back up. If previous pointed at latest and the registry has moved latest forward, rollback pulls the new image, not the old one. There is no safe rollback without immutable tags.

Native stacks: Artifacts are prebuilt binaries staged into versioned directories (releases/<version>/). zook never builds from source. Each releases/<version>/ directory is immutable once written — rollback works because the prior directory still exists on disk. Never overwrite or delete a release directory that may be the rollback target.

Good:

image: ghcr.io/example/api:v1.2.3
image: ghcr.io/example/api:a3f8c92

Not acceptable:

image: ghcr.io/example/api:latest
image: ghcr.io/example/api:stable

2. Healthchecks required for every long-running service

Docker stacks: Every long-running service in the stack must define a Docker healthcheck: in compose.yaml or a HEALTHCHECK in its Dockerfile. One-shot containers (migrations, seed scripts) are exempt.

Why this matters: docker compose up -d --wait only waits on services that have a healthcheck. A service without one is reported as healthy the instant its container starts — a false green. Zook's preflight check refuses to deploy any stack where a long-running service lacks a healthcheck, so the false-green case never reaches production.

Third-party images (databases, caches) often lack a built-in healthcheck; add one in compose.yaml:

postgres:
  image: postgres:16-alpine
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U postgres"]
    interval: 5s
    timeout: 5s
    retries: 10

Use only commands that exist inside the image. Do not assume curl or wget are present in minimal images.

Native stacks: Every native stack must have a health block in zook.yaml with exactly one of url or command. Zook's preflight refuses any native stack that lacks a health block — there is no docker healthcheck mechanism to fall back on, so this is non-negotiable.

Decision matrix (both runtimes):

Healthcheck Outcome
Present + passes SUCCESS — deploy committed
Present + fails within timeout ROLLBACK — previous version restored
Missing (docker) REFUSED at preflight — running state is not touched
Missing (native) REFUSED at preflight — running state is not touched

3. Backward-compatible migrations

Schema migrations must follow an expand/contract pattern: the new schema must remain compatible with the previous application version. If a deploy fails after a migration has run, auto-rollback brings the previous application code back — but it cannot un-run the migration. The previous application must still work against the new schema.

This is a people-and-process constraint, not one Zook enforces technically. Documenting it here makes it explicit for everyone operating a stack.

4. Fail-closed

Zook never marks a deploy successful unless it can confirm the new version is healthy. If health confirmation is impossible — due to a missing healthcheck, a timeout, or a container crash — Zook does not count it as a success. It either rolls back (if a previous version exists) or halts for manual intervention (if there is nothing to roll back to).

Rollback is itself health-gated. If restoring the previous version also fails health, Zook stops and prints:

DEPLOYMENT FAILED / ROLLBACK FAILED — manual intervention required.

No further automatic recovery is attempted.

Setting rollback_on_fail: false in zook.yaml disables auto-rollback for a stack. A failed deploy stops immediately and records failed in state.json, leaving the stack in a known-bad state for manual intervention.

5. VERSION injected as environment variable (docker stacks)

Zook sets VERSION in the environment when calling docker compose, never by writing to .env. Use ${VERSION} in compose.yaml to reference it:

services:
  api:
    image: ghcr.io/example/api:${VERSION}

Zook never modifies .env. Secrets and static configuration stay in .env, managed separately from the release lifecycle.

For native stacks, the version is the directory name under releases/ and the key substituted into the artifact URL template. It is not injected into the binary's environment.