The native runtime deploys prebuilt binaries managed by systemd. It uses the
same zook deploy / zook rollback interface as the docker runtime, with the
same health-gated, fail-closed guarantees — but it drives systemctl and
release directories instead of docker compose.
What the native runtime does (and does not do)
Does:
- Stage a versioned artifact into
releases/<version>/(by fetching a URL, or verifying a pre-staged directory). - Flip the
currentsymlink atomically toreleases/<version>/. - Run
systemctl restart <unit>. - Poll a health probe (HTTP or command) until the app passes or
health_timeoutelapses. - Auto-roll back (flip symlink back, restart, re-probe) on failure.
Does not:
- Build from source. Artifacts must be prebuilt and immutable.
- Generate or install systemd unit files. The operator installs the unit once; zook only restarts it.
- Supervise processes directly. systemd owns the lifecycle, boot persistence, restart-on-crash, and log capture.
Operator responsibility
Before the first zook deploy, install a systemd unit that points at the
stable current path:
[Unit]
Description=Rue API
[Service]
ExecStart=/opt/stacks/rue/current/rue-api
Restart=on-failure
[Install]
WantedBy=multi-user.target
/opt/stacks/rue/current is the symlink zook manages. The unit never needs
to change; zook flips the symlink before each restart.
Enable the unit once:
systemctl enable rue-api
Subsequent deploys are driven entirely by zook deploy rue <version>.
zook.yaml for native stacks
Every native stack requires a zook.yaml in the stack directory. Zook parses
this file strictly — unknown keys cause a parse error, so typos are caught at
preflight rather than silently ignored.
Full field reference
# Selects the runtime. "docker" is the default when this file is absent or the
# field is omitted.
runtime: native
# Path of the binary within releases/<version>/. Required for native.
binary: rue-api
# systemd unit name that zook restarts. Required for native.
systemd_unit: rue-api
# URL template for the release artifact. ${VERSION} is substituted at deploy
# time. Optional — omit when the artifact is pre-staged by CI.
artifact: https://ci.example.com/rue-${VERSION}.tar.gz
# Health probe. Required for native; exactly one of url or command must be set.
health:
# HTTP mode: GET the URL; healthy when the response status equals
# expected_status (default 200).
url: http://localhost:8080/healthz
expected_status: 200
# Command mode (alternative to url): run the command; healthy when it exits 0.
# command: ["rue-api", "health"]
# How long to wait for health before declaring the deploy failed.
# Accepts Go duration strings (e.g. 30s, 2m). Default: 60s. Applies to both
# runtimes.
health_timeout: 60s
# Whether to auto-roll back when a deploy fails health. Default: true. Applies
# to both runtimes.
rollback_on_fail: true
Validation rules
| Condition | Result |
|---|---|
runtime: native without binary |
Preflight refuses |
runtime: native without systemd_unit |
Preflight refuses |
runtime: native without a health block |
Preflight refuses |
Both url and command set in health |
Preflight refuses |
Neither url nor command set |
Preflight refuses |
Unknown key in zook.yaml |
Parse error on load |
Invalid health_timeout duration string |
Parse error on load |
Worked example
Stack: rue at /opt/stacks/rue/.
# /opt/stacks/rue/zook.yaml
runtime: native
binary: rue-api
systemd_unit: rue-api
artifact: https://releases.example.com/rue/rue-${VERSION}.tar.gz
health:
url: http://localhost:8080/healthz
expected_status: 200
health_timeout: 90s
rollback_on_fail: true
Deploy:
$ zook deploy rue v1.4.2
fetching https://releases.example.com/rue/rue-v1.4.2.tar.gz
current -> releases/v1.4.2
deployed rue v1.4.2
Artifact model
Native stacks use one of two artifact workflows. zook never builds from source; all artifacts are prebuilt and treated as immutable.
URL-fetch (recommended)
Set artifact: in zook.yaml to a URL template containing ${VERSION}.
On zook deploy <stack> <version>, zook substitutes the version and downloads
the artifact into releases/<version>/. If releases/<version>/ already
exists, the download is skipped (idempotent re-deploys).
Two archive shapes are supported:
.tar.gz/.tgz: extracted directly intoreleases/<version>/.- Single binary URL (no
.tar.gzsuffix): saved asreleases/<version>/<basename>and made executable.
The binary named by binary: in zook.yaml must be present in the extracted
directory after fetch; preflight verifies this.
Pre-staged
Omit artifact: from zook.yaml. Before running zook deploy, stage the
release directory manually (via CI, rsync, scp, or similar):
rsync -a ./dist/rue-v1.4.2/ server:/opt/stacks/rue/releases/v1.4.2/
On deploy, Pull verifies that releases/<version>/ exists and that the binary
is present. If neither artifact: is set nor the directory is pre-staged,
preflight refuses before touching the running stack.
On-disk layout
/opt/stacks/rue/
├── zook.yaml # runtime: native + config
├── releases/
│ ├── v1.4.2/
│ │ └── rue-api # immutable — fetched or pre-staged
│ └── v1.4.1/
│ └── rue-api
├── current -> releases/v1.4.2 # symlink; systemd's unit points at this
└── .zook/
├── state.json # release state (unchanged from docker layout)
└── logs/
└── 20260814T120000Z-v1.4.2.log
current is a relative symlink (releases/v1.4.2). zook flips it using an
atomic write-to-temp-then-rename, so a crash mid-flip never leaves a broken
symlink. Prior release directories are not deleted — rollback works because
releases/<previous-version>/ still exists on disk.
Deploy flow
zook deploy rue v1.4.2
Preflight: validate zook.yaml (binary, systemd_unit, health block present)
confirm artifact resolvable (URL configured OR releases/v1.4.2 staged)
│ missing → REFUSE — no changes made
▼
Pull: if artifact URL and releases/v1.4.2 absent → download + extract
else verify releases/v1.4.2/ exists and binary is present
▼
Up: flip current → releases/v1.4.2 (atomic)
systemctl restart rue-api
poll health (url or command) until healthy or health_timeout
│
├─ healthy → record success, write state.json
│
└─ unhealthy → auto-rollback:
flip current → releases/v1.4.1 (previous)
systemctl restart rue-api
poll health
ok → record rolled_back, exit non-zero
fail → record failed, HALT (manual intervention required)
rollback_on_fail: false skips the auto-rollback step and halts immediately
after the failed health poll, leaving state marked failed. Use this for
stacks where you prefer to intervene manually rather than have zook attempt a
restore.
zook rollback rue takes the same restore path on demand: flips current to
previous, restarts the unit, and health-gates the result.
Error handling summary
| Situation | Outcome |
|---|---|
| Preflight validation fails | Refused — no disk changes, exit non-zero |
| Artifact not found (no URL, not staged) | Refused at Pull, exit non-zero |
systemctl restart fails |
Up returns error → auto-rollback |
| Health poll times out | Up returns error → auto-rollback |
| Auto-rollback succeeds | rolled_back recorded, exit non-zero |
| Auto-rollback also fails | HALT — failed recorded, manual intervention message |
rollback_on_fail: false |
No auto-rollback — failed recorded, exit non-zero |