Skip to content

Deploying a fleet

punix service deploy deploys one stack to one target. punix fleet apply deploys many stacks across many hosts from one command — orchestration over the existing per-host deploy, not a new mechanism. Each host's deploy stays atomic and independently rollback-able.

The Fleet module

# fleet.pcl
module Fleet {
  hosts = [
    { file = "hosts/web.pcl"     stack = "WebStack"   target = "ssh://deploy@web.example.com" }
    { file = "hosts/db.pcl"      stack = "DbStack"    target = "ssh://deploy@db.example.com" }
    { file = "hosts/worker.pcl"  stack = "WorkerStack" target = "ssh://deploy@worker.example.com" }
  ]
}

Each host names a self-contained PCL file, the stack module inside it, and the deploy target. Host file paths are resolved relative to the fleet file.

punix fleet apply fleet.pcl --key ~/.ssh/punix-deploy

Failure isolation

By default a failing host is recorded and the run continues; the command exits 1 if any host failed, with a per-host report. --fail-fast stops at the first failure. Each host's result is one of four statuses: ok, failed (its own deploy raised), blocked (a host it depends on — via a cross-host artifact — failed or was blocked, so it is skipped without being attempted), or not-attempted (under --fail-fast, never reached).

host web.example.com    → deployed: gen-004
host db.example.com     → FAILED: [E11] secret(s) not set: from_vault:db_pw
host worker.example.com → deployed: gen-002

Flags

fleet apply takes the fleet file as a positional argument, plus:

  • --fail-fast — stop at the first failing host (default: continue).
  • --dry-runRealiseDryRun for every host (no real builds or writes).
  • --key / --known-hosts — SSH credentials for ssh:// targets.
  • --tls-certs DIR / --vault-secrets FILE / --activate — same as service deploy, applied per host.
  • --transport-root PATH — hermetic: deploy each host to <root>/<target>/ via LocalTransport instead of SSH (for tests and local runs).
  • --scenario NAME.

Cross-host wiring is config, not references

PCL has no imports, so each host is a separate evaluation — there is no fleet-wide reference resolution. A service on one host reaches another by address or secret (its config), never by a cross-module PCL reference. Don't expect WebStack to read a binding from DbStack.

Deploy ordering & cross-host artifacts

The one cross-host coupling that isn't a static address is a deploy-time artifact — a fact that only exists after a host deploys (a cert digest for DANE/TLSA, a DKIM key). A host producesArtifacts; another consumes them via an { artifact = { host, name } } reference. fleet apply builds the producer→consumer graph up front, deploys producers before consumers (stable topological order; a cycle is rejected before any host runs), and threads only the digest across — never the preimage. See Cross-host deploy-time artifacts for the full recipe.

Where in the code

  • src/punix/deploy/fleet.pyread_fleet, apply_fleet (pure orchestration, no Transport).
  • src/punix/cli.py — the fleet apply command wires it to the per-host deploy.