Transport and deploy targets¶
Punix lets you deploy to different places; your local machine, a remote SSH host, a sandbox directory for testing; by changing one configuration choice. The rest of the system (composition, realisation, manifests, rollback, GC) is unchanged across targets.
This page lists the deploy targets available today and how to pick one.
The shipping targets¶
Local filesystem (default)¶
Writes to the real filesystem. Most operations under /etc/systemd/system/ need root.
Remote host via SSH¶
punix service deploy MyStack \
--file stack.pcl \
--target ssh://deploy@prod.example.com \
--key ~/.ssh/punix-deploy
Punix connects to the target host via ssh, pushes the closure via rsync, writes config files via ssh + cat, and flips the live symlink via mv over ssh (which invokes rename(2) on the remote; same atomicity guarantee as the local case).
Strict host-key checking is always on. Pin a per-environment known_hosts with --known-hosts /path/to/known_hosts for CI / automated deploys.
Hermetic sandbox (for testing)¶
The entire filesystem surface is rebased under /tmp/test-host. /etc/systemd/system/api.service lands at /tmp/test-host/etc/systemd/system/api.service. No root required; no real /etc touched. Punix's own conformance suite uses this, and it's available for your own deploy validation.
What "transport-agnostic" means in practice¶
The same generation manifest, the same atomic-flip guarantee, the same rollback flow apply whether you're deploying locally or over SSH. The same code path runs everywhere. The IO travels through SSH on a remote host and through local syscalls on the local machine.
Operational consequences:
- Bugs surface uniformly. If atomic rollback works locally, it works over SSH. The conformance suite explicitly proves the same five atomic-update properties under both transports; if a bug did sneak into one but not the other, CI would catch it.
- You can practice rollback against a sandbox before you need it in anger.
--transport-rootlets you stage a full deploy/rollback round-trip locally without touching anything real. - Cross-host promotion is bandwidth-efficient. The closure push step asks the target "do you have this content-addressed path?"; if yes, it skips. Re-deploying the same stack across many machines transfers only the missing bytes.
Closure push: how it works¶
When you deploy over SSH, Punix walks the stack's pinned store paths and pushes anything the target doesn't already have:
The deploy summary tells you exactly how much had to travel:
1 pushed: one store path's worth of bytes was rsync'd to the target.4 cached: four store paths were already on the target (because they had the same content-hash); no transfer.
For "same machine" deploys (e.g. --target ssh://localhost to a localhost-sshd), every push is a cache hit: the deploy transfers zero bytes of closure.
For a real cross-host first deploy, every path is pushed. For incremental deploys after that, only changed paths transfer.
Limitations to be aware of¶
Activation is opt-in, not automatic¶
punix service deploy STACK writes the unit files and flips the symlink, and stops there. That is a valid end state. Pass --activate and it also runs systemctl daemon-reload then restarts each unit over the same transport, after the flip; --enable additionally makes them survive a reboot. See Activation.
Cross-arch deploys need a same-arch build farm¶
If your dev host is x86_64 and your target is aarch64, the closure has been built for the wrong arch. The deploy will succeed (the bytes push cleanly) but the services will fail at start time with "exec format error". For now: build on a host with the right arch (an aarch64 jumpbox), then deploy from there.
A future "build on the remote target" path is on the roadmap.
What resolves at deploy time¶
Three things, and no more: secrets (from_env / from_file / from_vault), cross-host artifacts (a value produced on one host and consumed on another; see cross-host artifacts), and the store paths the closure pins. Everything else is fixed when the config is evaluated.
Also not resolved at deploy time: authenticated source fetches. source = { url = "…", token = {from_env = "…"} } parses but the fetcher ignores it, because a fetch keyed on a secret cannot be cached without breaking hash exclusion. That one is on the roadmap.
Future targets¶
Punix ships local + SSH transports, and systemd as the only service backend. Planned backends:
- launchd for macOS daemons.
- supervisord for process supervision without systemd.
- docker-compose for container deployments.
Each is a new generator behind the same transport abstraction; the deploy / rollback / manifest contract doesn't change. The difficulty is not the renderer. The fields you write today are systemd-shaped, so a stack authored for systemd won't port unchanged until there's a neutral service vocabulary underneath.
Not on this list any more: user-mode systemd shipped as scope = "user": units in ~/.config/systemd/user/, activated with systemctl --user, no root. It is local-only for now: the home directory is resolved on the machine running punix, so a user-scope deploy over SSH is refused rather than guessed.
Related¶
- Deploy over SSH: operator how-to.
- Generations and rollback: the atomic flip that works the same on every target.
- Three pillars: the multi-backend property in context.