Target units are systemd’s modern replacement for SysVinit runlevels, offering a more flexible and robust way to manage system states.
Let’s see systemd targets in action. Imagine you want to boot into a graphical desktop environment.
sudo systemctl isolate graphical.target
This command tells systemd to stop all units not required by graphical.target and start all units that are required by it. If you were in a text-only mode (multi-user.target), you’d see your desktop environment load up. Conversely, to go back to a text-only mode:
sudo systemctl isolate multi-user.target
This stops graphical units and ensures only essential services for a multi-user system are running.
The core problem systemd targets solve is the rigid, often confusing nature of traditional runlevels. Runlevels were a fixed set of numbered states (0-6, S), and transitioning between them involved a complex chain of script execution. Systemd targets, on the other hand, are declarative units. They define a desired state by listing other units that must be active. This allows for more complex relationships and dependencies. For instance, graphical.target doesn’t just start graphical services; it requires multi-user.target to be active first, and then starts its own graphical dependencies.
Here’s how it works internally: Each .target unit is a .service file with Type=oneshot and RemainAfterExit=yes. When systemd encounters a target, it checks its Requires= and Wants= directives. Requires= means if the required unit fails, the target unit also fails. Wants= means systemd will try to start the wanted unit, but if it fails, the target unit will still be considered active. Systemd then recursively resolves all dependencies for the target unit and brings them all online.
The key levers you control are the .target files themselves and the .wants/ and .requires/ directories within /etc/systemd/system/. You can create custom targets or modify existing ones to define specific system states. For example, you could create a development.target that starts your web server, database, and IDE, and then make it depend on multi-user.target.
# Example custom target unit file: /etc/systemd/system/development.target
[Unit]
Description=Development Environment
Wants=nginx.service postgresql.service vscode.service
[Install]
WantedBy=multi-user.target
To activate this custom target on boot, you’d create a symlink:
sudo ln -s /etc/systemd/system/development.target /etc/systemd/system/multi-user.target.wants/development.target
Then you could manually start it:
sudo systemctl start development.target
Or set it as the default target for boot. The default target is usually determined by a symlink in /etc/systemd/system/default.target. For a typical desktop system, this points to graphical.target. For a server, it’s often multi-user.target. You can change this:
sudo systemctl set-default graphical.target # For graphical boot
sudo systemctl set-default multi-user.target # For text-only boot
The surprising thing most people miss is that targets aren’t just about starting services; they are about defining dependencies and ordering. When you isolate a target, systemd doesn’t just start the target’s direct dependencies. It recursively evaluates all dependencies for all units that are required or wanted by the target, ensuring a consistent and ordered state transition. This means a simple Requires=foo.service in a target might pull in dozens of other services and targets indirectly.
The next concept to explore is how to manage and troubleshoot service dependencies within this target-driven system.