On this page
Simple and Oneshot services
A service's type answers one question: does the process keep running, or does it run once and exit? peinit supports exactly two answers — Simple and Oneshot — set by the Type field (default Simple). The type is independent of when the service starts; a Oneshot can be boot-triggered, timer-triggered, or demand-only, exactly like a Simple service.
Simple services
A Simple service is a long-running daemon. peinit forks, installs the token, and execs the binary. The process is the service: while it runs, the service is running; when it exits, the service has stopped. This is the default and covers the large majority of services — registryd, authd, sshd, application daemons.
The interesting question for a Simple service is when does it count as started — the moment its dependents are allowed to begin. That is the readiness model, set by the Readiness field.
Readiness: Notify vs Alive
| Readiness | Ready when… | Use for |
|---|---|---|
| Notify (default) | The service sends READY=1 via sd_notify. |
Any service that has meaningful startup work — opening a socket, loading state, connecting to a backend. The dependent genuinely should wait. |
| Alive | The process exists — readiness is immediate. | Services with no startup handshake, where "the process is up" is as good a signal as you will get. |
The distinction matters because it is a promise to dependents. With Notify, a service that declares itself a Requires target is telling peinit "do not start anything that needs me until I say READY=1." With Alive, peinit can only promise that the process was forked — not that it is functional.
Readiness=Alive on a service that other services Requires is a common configuration smell. peinit will let it boot, but it logs a validation warning: Alive readiness gives no guarantee the service is actually serving before its dependents start. If a dependent connects on boot and gets connection-refused, this is usually why. Prefer Notify wherever the service can signal it.Once ready, a Simple service transitions to Active and stays there until its process exits or it is stopped. What happens on exit — clean exit, crash, restart — is the subject of The service lifecycle and Keeping services running.
Oneshot services
A Oneshot service is a run-to-completion task. peinit forks, installs the token, execs the binary, and waits for it to exit. It is the right type for setup work: creating directories, initialising a database, running a schema migration, applying a one-time fixup at boot.
For a Oneshot, readiness is exit, not a signal. The Readiness field is ignored entirely — READY=1 is meaningless for a process whose whole job is to finish. Success and failure are decided by the exit code:
- Exit 0 (or any code listed in
SuccessExitCodes) → the service succeeded. - Any other exit, or death by signal → the service failed, and the failure policy applies.
SuccessExitCodes is for binaries that signal a meaningful outcome with a non-zero code — for example, a tool that exits 2 to mean "nothing to do." Each entry is a decimal 0–255; code 0 is always success and need not be listed.
Completed, and RemainAfterExit
A successful Oneshot transitions to Completed, and what happens next depends on RemainAfterExit:
RemainAfterExit |
After a successful exit |
|---|---|
| 0 (default) | The service passes through Completed — releasing its dependents — then transitions to Inactive. |
| 1 | The service stays in Completed. |
Both forms satisfy dependents; the only difference is what a later status query shows. Use RemainAfterExit=1 when "this task is done" is a state worth seeing — a boot-time migration you want to confirm ran, say — rather than a service that quietly returns to Inactive.
Oneshot timing and hooks
Two details distinguish a Oneshot's start sequence from a Simple one:
StartTimeoutcovers the entire run. For a Simple service the timeout covers startup until readiness; for a Oneshot it covers everything from the first pre-hook to process exit. A long-running Oneshot must raiseStartTimeoutaccordingly (or sendEXTEND_TIMEOUT_USECas it works — see Keeping services running).ExecStartPostruns only on success. Post-hooks fire after the successful exit. If the Oneshot fails,ExecStartPostdoes not run.
Restarting a Oneshot
A Oneshot is not restarted on success, regardless of RestartPolicy — even Always. A successful exit is the goal, not a failure to retry. RestartPolicy governs only the response to failure (a non-zero exit). To re-run a Oneshot on a schedule, give it a timer trigger; that is the intended mechanism.
Simple vs Oneshot at a glance
| Simple | Oneshot | |
|---|---|---|
| Process lifetime | Long-running; is the service | Runs once, exits |
| Readiness | Notify (READY=1) or Alive |
Successful exit — Readiness ignored |
| Satisfies dependents when | Active | Completed (with or without RemainAfterExit) |
| Successful end state | Active (until stopped) | Completed, then Inactive (unless RemainAfterExit=1) |
ExecStartPost fires |
After readiness | After successful exit only |
StartTimeout covers |
Startup until readiness | The whole execution |
| Watchdog & health checks | Supervised while Active | None — WatchdogTimeout/HealthCheck are inert |
| Restart on success | n/a | Never (use a timer to re-run) |
The sd_notify contract
A Notify-readiness service signals readiness by sending READY=1 over the sd_notify protocol — a datagram to the socket named in the NOTIFY_SOCKET environment variable, which peinit sets for every service. READY=1 is only the most visible message; the same channel carries watchdog keepalives (WATCHDOG=1), graceful-stop notice (STOPPING=1), timeout extensions (EXTEND_TIMEOUT_USEC), status strings (STATUS=), and the fd store. These are covered where they belong — readiness here, the rest in Keeping services running and Controlling services.
Readiness is always per start generation. A READY=1 left over from a previous incarnation of the process is never valid for the current start — peinit ties every notify message to the specific process it is currently supervising, verified by pidfd.
Who is allowed to send these messages is governed by NotifyAccess, which currently has a single value: Main (0, the default). Under Main, only the tracked main PID — the process peinit forked and supervises — is authorised to send sd_notify readiness and status messages; a message arriving from any other PID (a child, a helper, an unrelated process) is ignored. So if a worker your service spawns sends READY=1, peinit will not act on it — the readiness signal must come from the main process itself.
No forking daemons
peinit does not support services that double-fork to background themselves. The whole reason a traditional daemon double-forks — to detach from the launcher — is moot when the launcher tracks the process it spawned. peinit obtains a pidfd for every process at fork time, which is a kernel handle to that exact process, immune to PID-reuse races. There is no MAINPID= mechanism for a service to redirect supervision onto a different process; peinit supervises what it forked.
If a legacy binary insists on daemonising, the fix lives at the packaging layer — wrap it so it stays in the foreground (commonly a --no-daemon/--foreground flag) — not at the init layer.
Where to start
To control when either type of service starts, read Triggers and timers.
To follow a service from definition to running process and read its states, read The service lifecycle.
To configure restart, health checks, and watchdogs for a Simple service, read Keeping services running.