Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

dinit Service Descriptions (dinit-service(5))

Overview

A dinit service is described by a plain-text file whose name (minus any @argument) is the service name. There is no XML, no INI section headers, no shell execution at load time — just property = value lines interpreted by the daemon, with dependency relationships expressed as first-class properties. The authoritative grammar is the dinit-service(5) man source; every claim on this page traces to it.

Files live in the service description directories searched in order — for the system instance /etc/dinit.d, /run/dinit.d, /usr/local/lib/dinit.d, and /lib/dinit.d; for a user instance $XDG_CONFIG_HOME/dinit.d, $HOME/.config/dinit.d, /etc/dinit.d/user, /usr/lib/dinit.d/user, and /usr/local/lib/dinit.d/user (per the FILES section of dinit(8)). Loading is lazy: a description is read when its service is first needed, and once loaded it stays loaded until explicitly unloaded.

This page covers the file grammar, the five service types, the dependency properties and their exact failure semantics, the process-management options, logging and socket options, variable substitution, and worked examples ending in a boot-profile layout.

File Grammar

Each line of a description file sets one property, in either of two forms:

property = value
property: value

The man page is precise about the distinction: there is currently no functional difference between the two assignment forms, but some properties override any previous setting of the same property while others are additive (dependencies and options in particular) — the recommendation is to use = for overriding settings and : for additive ones, so the file itself signals intent. A small set of properties can also be appended to with +=.

Other grammar rules:

  • Comments start with # and run to end of line; they must be separated from values by whitespace.
  • Whitespace collapses to single spaces; leading/trailing whitespace is stripped.
  • Double quotes group tokens: they prevent whitespace collapse and protect special characters (like #) inside them; the quotes themselves are not part of the value.
  • A backslash escapes the next character (even inside quotes). An escaped newline continues the value onto the next line (the continuation line must start with whitespace); \\ collapses to a single backslash.
  • Lines beginning with @ are meta-commands (see below), not properties.

Service names must be non-empty, must not begin with a dot, and consist of alphanumerics plus ., -, _ (non-ASCII bytes are also permitted). Service arguments extend the model: base-name@argument identifies an independent instance, whose argument is substituted wherever $1 appears in the description — one template file, many instances (a classic use is per-tty or per-interface services). All settings that name paths are best written as absolute paths; relative paths resolve against the description file’s directory, and the ---.d directory settings resolve against the containing fragment (relevant with @include).

Meta-commands: @include path inlines another file (error if missing), @include-opt path does the same but silently skips a missing file, and @meta enable-via service-name records which service dinitctl enable should attach this service to by default. @include is how distributions share a fragment across several descriptions without duplication.

Service Types

Five types, from the SERVICE TYPES section:

TypeBehaviorTypical use
processOne supervised process; service started/stopped state is linked to the process’s lifeDaemons with a foreground mode (sshd -D, nginx -g 'daemon off;')
bgprocessLike process, but for daemons that fork themselves: dinit waits for the original process to exit after forking, reads the new PID from pid-file, and supervises that processLegacy daemons that insist on self-daemonizing
scriptedStarted and stopped by executing a command to completion; successful exit means “started” (or “stopped”)Mounts, fsck, one-time setup — the oneshot equivalent
internalNo process at all; starts/stops purely as graph stateGrouping nodes, checkpoints, boot profiles (targets)
triggeredLike internal, but does not reach started state until an external trigger (dinitctl trigger)Device-appearance or event-gated readiness

The README adds the essential practical rule: for programs offering both modes, choose foreground and use process — supervision only works when the supervised process is the one dinit launched.

Dependency Properties and Failure Semantics

The three relationship types have sharply different semantics; the table consolidates the man page’s exact rules:

PropertyStart behaviorFailure of the dependencyDependency stops later
depends-on: (hard/need)Dependent waits for dependency to start; starting the dependent starts the dependencyDependent does not startDependent is stopped
prepared-by: (hard variant)Same as depends-on, but the named service is also restarted whenever this service restartsDependent does not startDependent is stopped
depends-ms: (milestone)Dependent waits for dependency to reach started; starting the dependent starts the dependencyDependent fails to start (returns to stopped)No effect — the relationship is satisfied once started
waits-for: (soft)Dependent waits for the dependency to finish starting or fail; starting the dependent still attempts to start itDependent starts anywayNo effect
after:Pure ordering: wait only if the named service is also currently starting; does not load or start itn/aNo effect
before:Mirror of after from the other siden/aNo effect
chain-to =When this service terminates on its own after starting, start the named servicen/a (the chain target is not even loaded until then)n/a

Directory forms aggregate dependencies from directory contents: depends-on.d:, depends-ms.d:, and waits-for.d: each name a directory whose entries (by file name) become dependencies of the corresponding type; entry contents are ignored, symlinks are the expected but not required form, and unreadable entries are non-fatal. The waits-for.d directory on the boot service is the canonical enable/disable mechanism — dinitctl enable writes a symlink there (see dinit in Operation).

chain-to deserves a note: by default the chain fires only when the service terminated of its own accord with a success status — not after an explicit stop, a dependency-driven stop, an abnormal exit, or when the service will restart; the always-chain option removes those guards. The documented uses are recovery shells (exit the rescue shell, the normal service starts) and multi-stage boot where later descriptions live on a filesystem mounted during the first stage.

DINIT-AS-INIT adds the operational guidance: prefer depends-on for real requirements — using waits-for or especially after for a genuine requirement just means dinit will try to start the service without its prerequisite and produce failure noise. Use after/before only when the named service may not exist at all.

Process-Management Options

The core options for process, bgprocess, and scripted services:

PropertyMeaningDefault
commandThe command line to start the process (also += appendable)required for process types
stop-commandExecuted to stop the service instead of signalling itsignal-based stop
working-dirWorking directorydirectory of the description file
run-asUser (and, by name, primary group + supplementary groups) to run asroot/root’s group
env-fileFile of NAME=VALUE assignments read at load time; values usable in substitutionnone
restartyes / no / on-failure (restart only on nonzero exit or deadly signal); inhibited after a user-initiated stopbuild-configurable
smooth-recoveryOn unexpected process death: restart the process in place, without stopping dependents or changing statefalse
restart-delayMinimum seconds between automatic restarts0.2
restart-limit-interval / restart-limit-countBurst guard: at most count restarts per interval (0 disables)10 s / 3
start-timeoutSeconds to start; on expiry SIGINT to the process group, then normal stopping; 0 = unlimitedbuild default (60 s in the example services)
stop-timeoutSeconds to stop; on expiry SIGKILL; 0 = unlimitedbuild default
term-signalSignal used to request termination (none or a name without the SIG prefix)TERM
pid-filebgprocess only: file where the daemon writes its PID before detachingrequired for bgprocess
ready-notificationpipefd:N or pipevar:NAME — wait for a write on the passed fd before “started”; pipefd form is s6-compatiblenone (started when execution begins)
rlimit-nofile / rlimit-core / rlimit-data / rlimit-addrspaceResource limits, soft:hard format (single value sets both)unchanged
niceCPU priority (also sets autogroup priority on Linux)unchanged
inittab-id / inittab-lineWrite/clear utmp entries for the process (needed for who-style output; doc recommends avoiding)unset

Build-gated additions (present when dinit is compiled with support, as Chimera does): run-in-cgroup = path (place the process in an existing cgroup; relative paths resolve against dinit’s own cgroup — with cgroups v2 a relative path typically begins with .. because of the no-internal-process rule; dinit does not create the cgroup), capabilities/securebits (Linux IAB capability syntax; ^CAP_NET_BIND_SERVICE in the Ambient set grants port-binding to unprivileged processes), ioprio, and oom-score-adj.

Variable substitution applies to command lines and most path values: $NAME or ${NAME}, with shell-like defaulting (${NAME:-word}, ${NAME-word}, ${NAME:+word}, ${NAME+word}), $$ for a literal $, and $1 for the service argument. Substitution happens at load time from the effective environment (service env-file > load-option exports > dinit’s process environment), so dinitctl setenv does not retroactively change already-loaded descriptions. $/NAME additionally splits the expanded value into multiple arguments. Dependency names undergo pre-load substitution (service argument and dinit environment only).

Logging, Sockets, and Console Options

Logging is configured with two properties. log-type = file | buffer | pipe | none selects the destination for the service’s stdout/stderr: append to logfile (created if absent; logfile-permissions default 600, with logfile-uid/logfile-gid for ownership — and note the failure mode: if the logfile’s directory does not exist or is not writable when the service starts, the service fails to start), keep an in-memory ring buffer (log-buffer-size caps it; read it with dinitctl catlog), or pipe it to another service (consumer-of = logger, where the logger is a process service with log-type = pipe whose stdin receives the stream). Specifying logfile alone flips the type to file. The pipe form is dinit’s answer to the supervision-world logger pattern, with the caveat that an un-consumed full pipe can stall the service.

Sockets: socket-listen = path makes dinit pre-open a UNIX socket and pass it to the service using the systemd activation protocol; socket-permissions (default 666), socket-uid, socket-gid control the socket node. The man page is explicit that this is not socket activation — no first-connection-triggered start — it only guarantees the socket exists and accepts connections (into a queue) from the moment the service starts.

Console options (set via the additive options property): runs-on-console gives the service exclusive console I/O (only one service may hold it; others queue), starts-on-console the same during startup only, shares-console non-exclusive access without delaying console-exclusive services, unmask-intr enables Ctrl-C delivery, start-interruptible allows SIGINT to cancel startup, and skippable makes an interrupt during a scripted startup count as success — the combination that makes an fsck script skippable at boot. Other options: starts-rwfs (this service makes the root filesystem writable — prompts dinit to create its control socket and log the boot to wtmp), starts-log (this service starts the syslog daemon — dinit flushes its buffered log through /dev/log from then on), pass-cs-fd (pass the control socket fd to the process — with a security warning: the service must close it before spawning untrusted processes), signal-process-only (signal the process, not its process group), kill-all-on-stop (scripted/internal only; before stopping, TERM-then-KILL every other process on the system — for shutdown scripts), no-new-privs, and always-chain.

Load options: load-options: export-passwd-vars exports USER, LOGNAME, HOME, SHELL, UID, GID into the service environment; load-options: export-service-name exports DINIT_SERVICE.

Worked Examples

A supervised daemon with readiness notification

# /etc/dinit.d/webapp
type = process
command = /usr/bin/webapp --listen :8080
run-as = webapp
smooth-recovery = true
restart = on-failure
ready-notification = pipefd:3
depends-on: rcboot
waits-for: syslogd
logfile = /var/log/webapp.log

Reading it: the process runs as webapp; if it dies unexpectedly it is restarted in place (dependents keep running); a crash-loop is bounded by the default 3-restarts/10-seconds limit; dinit does not report the service started until the app writes to fd 3; startup is ordered after syslogd (softly) and requires rcboot (hard).

A oneshot with dependencies and a chained rescue path

# /etc/dinit.d/rootfscheck  (adapted from the man page's example)
type = scripted
command = /etc/dinit.d/scripts/rootfscheck.sh
restart = false
options: starts-on-console pass-cs-fd
options: start-interruptible skippable
depends-on: early-filesystems
depends-on: device-node-daemon

A scripted service runs the command and is “started” when it exits 0. Because fsck can take arbitrarily long, the real example disables start-timeout (setting it to 0); because it must be interruptible, it holds the console and is marked skippable — Ctrl-C skips the check without failing the boot. pass-cs-fd lets the script’s reboot helper talk to dinit even though the control socket may not exist yet (the root filesystem is still read-only).

The boot profile pattern

# /etc/dinit.d/boot
type = internal
depends-ms: filesystems
depends-ms: loginready
waits-for.d: boot.d

The boot service is internal — pure graph state. Its hard milestone dependencies encode “boot has failed if these did not come up” (milestone rather than hard because, once the system is up, filesystems stopping should not tear the boot node down), and waits-for.d: boot.d points at a directory of symlinks: every entry is a service that boot waits for (softly) at startup. This is exactly the getting-started example extended to production shape, and it is the mechanism dinitctl enable/disable manipulates. The tty services then hang off loginready, an internal node that collects the services users need before login.

Boot and Shutdown Ordering — How the Graph Executes

On startup, dinit starts the named service(s) (default boot) and computes start order from the dependency graph: a service’s process is launched only after all its hard dependencies have started; dependencies themselves start (in parallel where possible) because starting a dependent implies starting what it needs. waits-for orders without coupling success; milestone dependencies order with success coupling until reached; after orders only co-starting services.

On shutdown, the order inverts through the same graph: a service is not signalled to stop until all of its dependents have completely stopped, and stopping a service stops its dependents first. The activation model ties this to runtime state: dinitctl start marks a service explicitly active; release clears the mark; a service that is neither explicitly active nor a dependency of an active service stops — so starting a service and then stopping it returns the system to its prior state, dependencies included. Failure propagation summarizes to the table above: hard dependencies stop their dependents; milestones do not (once reached); waits-for never propagates in either direction.

Interview Questions

Q: When would you use depends-ms (milestone) instead of depends-on (hard)?

When the dependency only needs to have happened, not to keep running. filesystems is the canonical example: mounting must complete (successfully) before services start, and it must have succeeded — a hard dependency would tear dependents down if the mount service ever stopped, but a milestone relationship is satisfied permanently once the milestone is reached. Conversely, a database is a depends-on dependency: if it stops, dependents must stop. The man page’s precise wording: with milestone, once the dependency reaches started state, it may stop without affecting the dependent — but failure to start, or cancelled startup, fails the dependent.

Q: What is the exact difference between waits-for and after?

waits-for creates a relationship: starting the dependent also starts the named service and delays the dependent’s start until that service finishes starting or fails. after creates no relationship: it only says “if the named service happens to be starting at the same time, let it finish first” — it neither loads nor starts it, and it does not even require the service to exist. DINIT-AS-INIT’s guidance follows: use depends-on for requirements, waits-for for best-effort ordering of things you are starting anyway, and reserve after for services whose existence you cannot guarantee (where a waits-for on a missing service would be a load failure).

Q: A process service with ready-notification = pipefd:3 never reaches STARTED state. What are the likely causes?

First, verify the daemon actually supports the protocol — writing a readiness byte to fd 3 must be implemented by the program (or s6-compatible behavior); a daemon that ignores fd 3 hangs the startup until start-timeout expires, whereupon dinit SIGINTs it and it enters stopping state. Second, check the fd number: if the service itself opens files, fd 3 could be consumed by the child; pipevar:NAME avoids the guessing by passing the fd number in an environment variable. Third, look at the transition markers in dinitctl list — the << indicator with no state change over the timeout window is the signature of a notification mismatch. This “waits the full start-timeout then fails” pattern is the classic dinit debugging scenario.

Q: How do dinit’s dependency directory forms (waits-for.d etc.) work, and why do they use file names instead of file contents?

waits-for.d: boot.d declares that every non-dot entry in the directory boot.d adds a waits-for dependency named after the entry. Contents are irrelevant by design — the presence of a name is the datum — which makes the directory a natural target for symlinks (a package manager links a service description into the profile directory to enable it) and for dinitctl enable/disable, which create and remove exactly such symlinks. Failure to read the directory or find a named service is explicitly non-fatal, so a stale symlink degrades gracefully instead of failing the boot.

Q: Where does run-as differ from OpenRC’s command_user or runit’s chpst -u?

In supplementary-group and lookup semantics, per the man page: run-as specified by name sets the primary group to the user’s primary group and initializes supplementary groups from the system group database; specified numerically, it keeps dinit’s own group and drops all supplementary groups. OpenRC’s command_user (evaluated by openrc-run before launching) and runit’s chpst -u behave similarly in the name form (chpst -u user:group can set an explicit group or colon-separated group list), but they run inside a shell script context, whereas dinit performs the switch in the daemon itself before exec — there is no script stage to inherit environment or umask from. For privilege dropping with capability trimming, dinit additionally offers capabilities/securebits/no-new-privs, which the shell-based systems approximate poorly.

References

Cross-References