systemd Dependencies for a Home Server - Separate Requirement from Ordering
A service should start after another service. That sounds like one rule, but systemd hears at least two separate questions: should starting the first unit also start the second, and if both are starting, which one goes first? Many confusing unit files begin by answering only one of those questions.
The distinction matters on a small home server. A web process may use a local database, an indexing job may need a mounted directory, or a backup task may contact a remote machine. Adding After= can make a boot graph look orderly while leaving the required unit out of the transaction. Adding Requires= can pull both units in while allowing them to start concurrently. Neither directive, by itself, means “wait until the application is healthy.”
This article develops a narrow mental model for the common directives Wants=, Requires=, After=, and Before=. It also explains where that model stops being enough.
Two axes, not one dependency ladder
The clearest starting point is to separate requirement from ordering. The systemd.unit manual describes these as independent, orthogonal relationships.
Wants=andRequires=answer whether another unit should be pulled into the same start transaction.After=andBefore=answer how jobs should be ordered when both units are in that transaction.
Think of a workshop rather than a queue. A requirement directive puts another worker on the attendance list. An ordering directive says who must finish a startup step before someone else begins. A name on the schedule does not invite that worker, and an invitation does not assign a place in the schedule.
This yields four useful cases:
Configuration in app.service | What it expresses |
|---|---|
After=store.service | If both units start, order the app after the store. It does not pull the store in. |
Wants=store.service | Try to start the store too, but do not impose an order. |
Wants=store.service plus After=store.service | Try to start both and order the app after the store. |
Requires=store.service plus After=store.service | Start both with a stronger requirement, and do not start the app if the ordered store activation fails. |
Wants and Requires differ in failure policy
Wants= is the weaker relationship. When the configuring unit starts, systemd also attempts to start the wanted units. A wanted unit that cannot be added or fails to start does not invalidate the overall transaction. Upstream documentation calls this the recommended way to connect one unit's startup to another in many cases.
Requires= makes a stronger statement. Starting the configuring unit also activates the required unit. When it is combined with After=, a failure to activate the required unit prevents the dependent unit from starting. Explicitly stopping or restarting the required unit also propagates that action to the requiring unit.
That is still not a permanent health contract. The manual notes cases in which a required unit can later become inactive without automatically taking the requiring unit down. A cleanly exiting service and a failed unit condition are among the nuances. Stronger relationships such as BindsTo= exist, but reaching for them automatically can make an installation brittle. The practical question is not “which directive is strongest?” but “must this service be withheld when that particular activation fails?”
For an optional local metrics collector, Wants= may preserve useful service when metrics are unavailable. For an application that cannot even initialize without a local store, Requires= with After= may describe the intended failure policy. The unit graph should express a real operational requirement, not anxiety about every possible failure.
After and Before only sequence participating units
If web.service declares After=store.service, systemd delays the web service's startup until the store's startup job has completed, provided both jobs are present. The inverse statement, Before=web.service in the store unit, describes the same ordering edge from the other side.
Ordering also affects shutdown. When two units with an ordering relationship are stopped together, systemd applies the reverse order. A unit configured After= another is stopped before that other unit. This is useful when a client should have an opportunity to close its work before the provider disappears.
There is a target-specific wrinkle. According to systemd.target, a target normally complements its own configured Wants= and Requires= relationships with After=. The reverse is not automatic: a service that says Wants=some.target should still add an explicit ordering directive when it needs one. This default should not be generalized into “requirements always imply ordering.”
A minimal pair of units
Consider two deliberately artificial services. They run sleep, not a real application, so the example demonstrates unit semantics without claiming to configure a database or web stack.
# dependency-demo-store.service
[Unit]
Description=Dependency demo store
[Service]
Type=exec
ExecStart=/usr/bin/sleep infinity
# dependency-demo-web.service
[Unit]
Description=Dependency demo web process
Requires=dependency-demo-store.service
After=dependency-demo-store.service
[Service]
Type=exec
ExecStart=/usr/bin/sleep infinity
[Install]
WantedBy=multi-user.target
Starting dependency-demo-web.service pulls in the store because of Requires=. The After= line separately orders the web process after the store's activation. Removing Requires= would leave only an ordering relationship; starting the web unit alone would not necessarily start the store. Removing After= would keep the requirement but allow the two startup jobs to proceed without that sequence.
The example can be checked before installation:
systemd-analyze verify \
dependency-demo-store.service \
dependency-demo-web.service
systemd-analyze verify is useful for finding unknown directives, missing dependencies, and other unit-file problems. Silence is not proof that the application-level relationship is correct; the tool can validate the graph and syntax more readily than the operator's intent.
Ordered does not mean ready
The most important limitation is hidden inside the phrase “finished starting.” For a service unit, that moment depends on its Type=. The systemd.service manual explains that the default Type=simple can be considered started immediately after the process is forked, even before its executable has successfully run. Type=exec waits until executing the service binary succeeds, but it still does not know whether an HTTP listener is accepting requests or a database has completed recovery.
A service with native systemd notification support can use Type=notify and send READY=1 when its own initialization is complete. Other designs avoid the race through socket or bus activation. Where neither is available, the dependent application often needs retries with bounded backoff. Adding a sleep to ExecStartPre= merely guesses how long readiness takes; it can waste time on a fast boot and still fail on a slow one.
This is why After=mariadb.service, for example, should not be paraphrased as “the database will accept this application's query.” It says that systemd observed MariaDB's defined activation boundary before beginning the dependent startup. Whether that boundary is sufficient must be checked against the actual provider service and client behavior.
The network shows why synchronization points are local promises
Network targets make the boundary especially visible. The systemd project's network synchronization guidance says that network.target only indicates that the network-management stack has started; it does not define configured connectivity. network-online.target actively waits for the network manager's definition of “up,” but that definition depends on local software and configuration.
Even reaching the online target is a historical event, not a guarantee that DNS, a remote host, or the wider Internet remains reachable. A network client that matters after boot still needs to handle lost connectivity. Pulling in network-online.target can be appropriate for a client that cannot start without initial network configuration, but applying it to every network-facing service can add delay without creating lasting reliability.
WantedBy belongs to installation, not service readiness
The example's WantedBy=multi-user.target appears under [Install]. systemd does not read that section as a runtime instruction during normal activation. Instead, systemctl enable uses it to create a symlink in a .wants/ directory. That makes the target pull in the service on a future boot or target activation.
Three operations are therefore easy to confuse:
systemctl enable example.servicechanges enablement links; it does not start the service unless--nowis also requested.systemctl start example.serviceactivates it now; it does not necessarily enable future boot activation.WantedBy=describes links that enablement should create; it does not replaceAfter=or an application readiness protocol.
Inspect the graph before changing it
A unit's effective relationships can come from its main file, drop-ins, enablement links, defaults, and implicit dependencies. Reading one file may therefore be incomplete. These commands expose different parts of the result:
systemctl cat dependency-demo-web.service
systemctl show dependency-demo-web.service \
-p Wants -p Requires -p After -p Before
systemctl list-dependencies dependency-demo-web.service
systemd-analyze critical-chain dependency-demo-web.service
systemctl cat shows the unit and its drop-ins. systemctl show exposes selected effective properties. list-dependencies helps inspect what is pulled in, while critical-chain focuses on time-critical ordering. None substitutes for testing failure cases: start the dependent when the provider is stopped, make the provider activation fail safely in a test environment, and observe shutdown or restart behavior. Production services should not be broken merely to satisfy a tutorial.
Prefer the smallest truthful graph
More edges do not automatically make a service more reliable. They can serialize work that could run concurrently, make boot depend on optional components, or spread one failure across otherwise useful services. Upstream systemd documentation explicitly recommends using dependencies sparingly and considering socket- or bus-based activation where those designs fit.
A practical review can ask four questions:
- Should activating this unit also activate the other one? If yes, choose a requirement relationship.
- Should this unit still start when the other activation fails? That separates many
Wants=cases fromRequires=cases. - If both participate, is their startup or shutdown order meaningful? If yes, add
After=orBefore=explicitly. - What observable event means the provider is actually ready, and does its service
Type=represent that event? If not, ordering alone is insufficient.
The useful lesson is not a magic pair of directives. It is the habit of naming the promise precisely. Requirement, order, enablement, and readiness are different promises. A unit file becomes easier to reason about when each line makes only the promise systemd can actually keep.
References
- systemd.unit - Unit configuration, systemd project.
- systemd.service - Service unit configuration, systemd project.
- systemd.target - Target unit configuration, systemd project.
- Running Services After the Network Is Up, systemd project.
- systemd.unit(5) - Debian bookworm, Debian Manpages.
