The systemd service surface¶
Composing a stack covers the slim Service (name/package/binary/args/dependsOn/environment). A real service needs more: a custom User, a StateDirectory, Restart=always, a oneshot on a timer, a system user created before it starts. Punix exposes those as a typed passthrough to systemd — not a curated subset, and not raw unit fragments.
serviceConfig — [Service] directives, typed¶
Any [Service] directive can be set via serviceConfig. Keys are freeform (systemd validates them at unit load; Punix validates only that they render); values are Str, Bool (→ yes/no), or Int.
services = [{
name = "appview"
package = "Appview"
binary = "appview"
serviceConfig = {
DynamicUser = true
StateDirectory = "appview"
Restart = "always"
RestartSec = 5
LimitNOFILE = 65536
}
}]
renders into the unit as:
[Service]
ExecStart=/store/<hash>-appview/bin/appview
Type=simple
Restart=always
DynamicUser=yes
LimitNOFILE=65536
RestartSec=5
StateDirectory=appview
serviceConfig is merged over the defaults Type=simple, Restart=on-failure (the caller wins). Setting Type=oneshot drops the default Restart (systemd forbids it there). Keys are emitted sorted, so the unit is byte-identical across permuted source.
Two keys are reserved — serviceConfig rejects them with a located error:
ExecStart— computed from the package store path +binary+args(the point of content-addressing).Environment— use the dedicatedenvironmentfield, so secret-resolution and hash-exclusion stay enforced.
A directive value with a raw newline (or a key with =/whitespace) is rejected — each directive is one Key=Value line. Encode such values at the source.
enable — omit a service¶
enable = false (default true) drops the service entirely: no unit file, not started, and not a valid dependsOn target (a dependency on a disabled service is a compose error). There is no render-but-mask.
tmpfiles — tmpfiles.d rules¶
Per-service tmpfiles rules are collected stack-wide (deduplicated, sorted) into /etc/tmpfiles.d/punix-<stack>.conf. systemd-tmpfiles applies them idempotently.
systemUsers / systemGroups — sysusers.d¶
Stack-level user/group creation renders to /etc/sysusers.d/punix-<stack>.conf (systemd-sysusers, idempotent, applied before services start). A user's group is its primary group; every referenced group gets an explicit declaration.
module MyStack {
backend = "systemd"
systemGroups = ["git"]
systemUsers = [{ name = "git" group = "git" home = "/var/lib/git" }]
services = [ ... ]
}
Names are validated as plain unix names ([a-z_][a-z0-9_-]*, ≤32); home must be an absolute path with no whitespace or quotes.
Timers and oneshots¶
A service with a timer record becomes timer-activated: the generator emits a sibling <name>.timer unit (WantedBy=timers.target) and the <name>.service drops its [Install] block (it is pulled in by the timer, not at boot). A oneshot is just serviceConfig = { Type = "oneshot" } — a timer + oneshot is the canonical periodic job.
{ name = "store-gc" package = "Punix" binary = "punix" args = ["store", "gc"]
serviceConfig = { Type = "oneshot" }
timer = { OnCalendar = "daily" Persistent = true } }
Unit= is reserved in timer — a foo.timer triggers foo.service by naming convention. OnCalendar values are passed through verbatim (systemd validates them).
Heterogeneous services: the module form¶
PCL lists are homogeneous, so a services = [ ... ] of inline records must all have the same field shape — two services with different serviceConfig keys won't unify. To mix shapes, reference services by module name (like packages):
module Api { package = "Api" binary = "api" serviceConfig = { DynamicUser = true } }
module Worker { package = "Worker" binary = "worker" serviceConfig = { Restart = "always" } }
module MyStack {
backend = "systemd"
services = ["Api", "Worker"] # module names, not inline records
}
Each service module is typed independently. A module's name defaults to the module name. A list is all-strings or all-records — never mixed (which PCL homogeneity already enforces).
Where in the code¶
src/punix/deploy/stack.py— theServicesurface + compose-time validation.src/punix/deploy/generators/systemd.py—render(stack)(units, timers, sysusers.d, tmpfiles.d).
Related¶
- Composing a stack — the basics.
- Stateful services —
StateDirectoryand the data/rollback boundary. - Activation — actually (re)starting the units.
- Reference: decisions — ADR-009 (service surface), ADR-010 (named modules), ADR-011 (timers).