How It Works
Booting at Startup
sysg supervises services; nothing supervises sysg. After a reboot the machine comes back with no supervisor and no services unless the platform's service manager — systemd on Linux, launchd on macOS — has been told to start one.
sysg install-boot writes that unit for you, from the manifest you point it at:
$ sysg install-boot --config /opts/app/sysg.prod.yaml
It prints the unit and where it would go. Nothing is written until you add
--write, and nothing is activated until you add --enable.
Why the unit runs start --attached
A service manager can only supervise the process it started. sysg start and
sysg start --daemonize both fork a supervisor and leave the invoking process
behind as a client, so a manager tracking that client is watching the wrong
process: it sees a clean exit when the supervisor dies, never restarts it, and
sends its stop signal somewhere the supervisor will never read it.
start --attached doesn't fork. This process is the supervisor, so:
- the manager's
Restart=fires on a real supervisor crash, - its stop signal reaches sysg's own graceful teardown, which stops services in reverse dependency order,
- and its exit status is the supervisor's exit status.
--attached refuses to run when a supervisor is already resident
(SG0007) — it cannot adopt one it did not
start, and neither can the manager.
Scopes
--scope user | --scope system | |
|---|---|---|
| Linux | systemd --user unit under ~/.config/systemd/user/ | system unit in /etc/systemd/system/ |
| macOS | LaunchAgent under ~/Library/LaunchAgents/ | LaunchDaemon in /Library/LaunchDaemons/ |
| Runs as | the invoking user | root, or the --run-as account |
| Starts at | that user's login | boot |
The default follows the runtime: --sys gives system, everything else gives
user.
Warning
A systemd --user unit starts at login and stops at logout unless lingering
is enabled for that account. install-boot prints the
loginctl enable-linger <user> step; without it, a reboot leaves the services
down until someone logs in.
Linux without systemd
install-boot renders two things: systemd units and launchd plists. It refuses
to write either onto a machine that runs neither
(SG0706) — on a host booted by runit, s6,
OpenRC or a bare container entrypoint, a systemd unit is a file nothing reads.
Rendering one is still allowed, since a unit generated on a build box is the
right unit for the systemd host it gets copied to; only writing it here is not.
The supervisor itself needs none of this. start --attached does not fork, so
whatever runs it supervises it:
$ sysg start --attached --config /opts/app/sysg.prod.yaml
Point your own service manager at that line and give it what the generated unit
would have set: the account to run as, HOME for that account, the manifest's
directory as the working directory, restart-on-failure, and a stop that sends
SIGTERM to that process and waits. sysg does the rest — the signal reaches its
graceful teardown, and services stop in reverse dependency order.
Container PID 1 is a different job. Use sysg init,
which adds orphan reaping that a supervisor started as any other PID cannot do.
Running as a non-root account
A machine-wide unit that runs the supervisor as an ordinary user — the common
shape for an app deployed under its own account — is a system-scope unit with
--run-as:
$ sudo sysg install-boot \
--config /opts/app/sysg.prod.yaml \
--scope system --run-as app --write --enable
That renders User=app and Environment=HOME=/home/app, read from the
passwd database. The HOME line is not decoration: the user runtime resolves
its state directory from HOME, and a User= unit that does not set it would
put every project's state under /.local/share/systemg.
A system-scope unit must say whose runtime it boots — either --sys for the
root-owned system runtime or --run-as for an account. Naming neither is
refused with SG0706 rather than quietly
running the user runtime as root.
What the generated unit sets, and why
[Unit]
After=network.target
StartLimitIntervalSec=60
StartLimitBurst=3
[Service]
Type=simple
ExecStart=/usr/bin/sysg --sys start --attached --config /etc/systemg/systemg.yaml
WorkingDirectory=/etc/systemg
KillMode=mixed
TimeoutStopSec=90
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Type=simple, noPIDFile— the tracked process is the supervisor, so there is no pidfile to chase.- No
ExecStop— systemd's SIGTERM already reaches the supervisor's graceful teardown, andTimeoutStopSecgoverns how long that gets. AnExecStoprunningsysg stopwould impose its own shorter deadline. KillMode=mixed— SIGTERM goes to the supervisor alone, so sysg stops services in its own order; anything still alive at the timeout is killed by cgroup.control-groupwould SIGTERM every service directly and destroy that ordering.WorkingDirectory— the manifest's own directory, so relative commands and script paths resolve the same way they do when you runsysg startby hand there. Override it with--workdir.StartLimitBurst— a manifest that fails on boot stops after three attempts instead of restarting forever.After=network.targetis ordering, not readiness. If services need a configured address, pass--wait-onlineforWants=/After=network-online.target. launchd has no equivalent, so--wait-onlineis refused there rather than silently dropped.
On macOS the same decisions come out as RunAtLoad, ThrottleInterval,
ExitTimeOut, and KeepAlive: { SuccessfulExit: false } — restart the
supervisor when it fails, never when it exited cleanly.
Note
launchd has no equivalent of StartLimitBurst. A manifest that fails at every
boot stops after three tries under systemd, but launchd keeps retrying it every
ThrottleInterval seconds indefinitely. A failure that happens before the
supervisor starts never reaches sysg logs — read the plist's
StandardErrorPath, <log dir>/<name>.err.log, instead.
Replacing a unit
Every generated unit carries a marker line naming the sysg version that wrote
it. --write replaces a unit carrying that marker and refuses to touch one
without it (SG0706), so a hand-written unit at
the same path is never overwritten.
Regenerate after moving the binary or the manifest: the unit names absolute
paths, and a stale ExecStart is a unit that fails at boot.