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. It orchestrates over the same per-host deploy path, so 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-run: RealiseDryRun 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: deploy each host to <root>/<target>/ via LocalTransport instead of SSH (for tests and local runs).
  • --scenario NAME.

Cross-host wiring through config

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. The preimage never crosses the wire. See Cross-host deploy-time artifacts for the full recipe.

Where in the code

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