Skip to content
daniel@cosenza:~/blog
LinuxDeep Dive April 14, 2026 3 min read

Understanding systemd: Units, Targets, and the Modern Linux Init System

How systemd's unit model replaced sequential init scripts with declarative, dependency-driven service management.

systemd has been PID 1 on most mainstream Linux distributions for over a decade now, replacing the sequential SysV-style /etc/init.d scripts with a declarative, dependency-graph-driven model. Whatever one thinks of the debates around its scope, its core design — units, targets, and parallel dependency resolution — solved real problems that shell-script-based init genuinely had.

Units: the atomic building block

Everything systemd manages is a unit, a plain-text configuration file describing one thing to start, stop, mount, or otherwise manage. The type is encoded in the file extension: .service for daemons, .mount for filesystem mounts, .timer for scheduled activation, .socket for socket-activated services, and more.

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target postgresql.service
Requires=postgresql.service

[Service]
ExecStart=/usr/local/bin/myapp
Restart=on-failure
User=myapp

[Install]
WantedBy=multi-user.target

[Unit] declares metadata and ordering/dependency relationships, [Service] (or the equivalent section for other unit types) declares the actual behavior, and [Install] declares how the unit attaches to the dependency graph when enabled.

Ordering vs. requiring: After/Before vs. Requires/Wants

A common early confusion is conflating ordering with dependency. After= and Before= only control the sequence in which units start relative to each other — they say nothing about whether one unit’s failure or absence should affect another. Requires= and Wants= express actual dependency: Requires=postgresql.service means systemd will try to start PostgreSQL as a prerequisite, and by default a failure there affects the dependent unit; Wants= expresses the same intent but without that failure propagation.

[Unit]
Wants=network-online.target
After=network-online.target

This pairing — Wants= plus After= on the same target — is the standard idiom for “start this after networking is up, but don’t refuse to start if networking somehow isn’t.”

Targets: synchronization points, not runlevels

.target units are systemd’s replacement for SysV runlevels, but they’re better understood as named synchronization points in the dependency graph rather than numbered stages. multi-user.target and graphical.target are the most commonly referenced, and a target itself does nothing but group other units together:

systemctl get-default
systemctl set-default multi-user.target
systemctl isolate rescue.target

systemctl isolate stops every unit not required by the target being switched to and starts everything that is — the mechanism behind runlevel-like transitions (switching to rescue or emergency mode) without any of SysV’s rigid numeric ordering.

Managing services

The day-to-day interface is systemctl, uniform across every unit type:

systemctl start myapp.service
systemctl enable myapp.service     # symlinks into the target's .wants directory
systemctl status myapp.service
systemctl daemon-reload            # re-read unit files after editing them

enable doesn’t start a unit immediately — it creates a symlink from the target’s .wants/ directory (e.g. /etc/systemd/system/multi-user.target.wants/myapp.service) to the unit file, which is what causes it to start on the next boot when that target is reached. This symlink-based mechanism is also why systemctl list-dependencies multi-user.target can show you the entire boot-time dependency tree at a glance.

Reading logs: journalctl

systemd’s logging component, journald, captures structured logs from every unit (plus kernel messages), queryable without needing to know which flat log file a given service happens to write to:

journalctl -u myapp.service -f
journalctl -u myapp.service --since "1 hour ago"
journalctl -b -p err              # errors and above, since last boot

Socket and timer activation

Two systemd features worth knowing specifically: socket activation (a .socket unit binds a port and starts its paired .service only when a connection actually arrives, deferring startup cost until it’s needed) and timers (.timer units, systemd’s declarative alternative to cron):

# myapp.timer
[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
systemctl list-timers

Debugging boot-time ordering problems

When a unit isn’t starting in the order you expect, systemd-analyze is the tool built specifically for this:

systemd-analyze blame              # which units took longest to start
systemd-analyze critical-chain myapp.service

critical-chain in particular walks backward from a unit through its After= dependencies, showing exactly which chain of prerequisites determined when it was allowed to start — usually the fastest way to find an unintended ordering dependency that’s delaying boot.