Skip to content

What the target needs

Ansible's strongest card is that a managed host needs nothing but sshd. Punix's position is a gradient, and the shallow end of it matches Ansible exactly: rollback Ansible does not have.

The pessimistic version ("you need Punix on every box") is what most people assume, and it is wrong for the case most people start with.

The gradient

what you deploy what the target needs what you get
config only: config files, dataFiles, service units, secrets sshd + rsync. Nothing else. No Punix, no Python, no agent, no daemon. generations, one-second byte-exact rollback, provenance, drift detection
+ prebuilt artifacts, same platform as your control node the above; the closure arrives by rsync reproducible, content-addressed binaries
+ a from-source build, target platform ≠ control node Punix installed on the target the full build pillar on a foreign platform

Only the third row needs anything installed, and it needs it for a reason that is a theorem rather than an oversight: realise happens on the control node, and a macOS binary does not run on Linux. There is no cross-realise, so a target of a different platform has to build for itself.

The row worth knowing about

A config-only stack deploys to a stock Debian box with nothing on it.

module MailConfig {
  backend = "systemd"
  configFiles = [
    { path = "/etc/postfix/main.cf"  source = "./postfix/main.cf" },
    { path = "/etc/dovecot/dovecot.conf"  source = "./dovecot/dovecot.conf" }
  ]
  services = ["PostfixSvc", "DovecotSvc"]
}
punix service deploy MailConfig --file mail.pcl \
  --target ssh://root@mail.example.com --activate

That host has never heard of Punix. It gets:

  • a numbered generation recording every file's sha256, the source hash, and which recipe produced what;
  • punix service rollback MailConfig: one syscall, byte-exact, and it works even if you delete the PCL;
  • punix verify MailConfig: "is this host still what I declared?", answered without an agent;
  • punix service why MailConfig /etc/postfix/main.cf: which recipe wrote this file, and a hash of that recipe's code.

The daemons themselves come from the distro's packages. Punix is managing their configuration, not replacing them. This is the shape the four-daemon mail stack ran in.

Versus Ansible, precisely

Ansible Punix, config-only
target prerequisites sshd (+ Python for most modules) sshd + rsync
declaration recorded on the host ❌ none gen-NNN.json
undo re-run an older playbook and hope ✅ one syscall, byte-exact
drift detection ❌ (no record to diff against) punix verify
provenance per file ✅ producing recipe + its code hash
orchestrating change on existing hosts ✅ its actual job ➖ not what this is

The last row marks the boundary. Ansible orchestrates change; Punix replaces a declared state atomically. If you need rolling restarts across a mixed inventory with per-host conditionals, use Ansible. If you need "this host is exactly this, and I can put it back", this is the trade.

Installing on the target, when you do need to

Since 0.3.0 the wheel depends on cyclopts and tabulate and nothing else: no compiler, no git, no evaluator you never invoke:

pip install punix          # or: uv tool install punix

Earlier versions pulled MIXINv2 from a pinned git source, which made git a real prerequisite. That is gone; see architecture.