Targets and Boot
Topic 43

Targets and Boot

systemdBoot

A systemd target is a unit that groups other units and acts as a synchronization point during boot. It carries no process of its own — multi-user.target does not run anything, it just declares "the system has reached the state where all the services a server needs are up." Targets replaced SysV runlevels, but they are not numbered slots you flip between; they are named nodes in a dependency graph, and a target can pull in any number of other targets.

Boot is that graph being resolved. systemd starts at a single default.target, walks every Wants and Requires edge to find everything it must activate, and starts those units in parallel, constrained only by the explicit Before/After ordering you declare. The operational consequence: when a server hangs on boot or comes up missing a service, you are debugging a graph, not a linear script — and the tools systemd gives you (systemctl list-dependencies, systemd-analyze) exist to make that graph legible.

Targets as Unit Groups

A .target unit exists to be depended on. It bundles a set of services and other targets under one name so you can refer to "the multi-user state" rather than enumerating thirty services. The two you meet first are multi-user.target — full system, networking, all daemons, no graphical login — and graphical.target, which pulls in multi-user.target and then adds the display manager on top. On a server you want the former; the latter drags in X or Wayland you have no reason to run.

Membership is expressed two ways, and the distinction matters. A unit listed under a target's Wants= is started alongside it but its failure does not fail the target; a unit under Requires= is mandatory, and if it fails the target is considered failed and its dependents are torn down. In practice most service-to-target links are Wants, created automatically when you systemctl enable a service — enabling drops a symlink into the target's .wants/ directory.

# what does multi-user.target actually pull in?
systemctl list-dependencies multi-user.target

# the enable symlink that makes a service part of the target
systemctl enable nginx.service
# -> /etc/systemd/system/multi-user.target.wants/nginx.service

The Default Target

default.target is the unit systemd activates at the end of boot, and it is nothing more than a symlink. On a Debian or Ubuntu server it points at multi-user.target; on a desktop install it points at graphical.target. The old runlevels map onto these targets through compatibility symlinks — runlevel3.target is multi-user.target, runlevel5.target is graphical.target — but the mapping is a convenience, not a one-to-one identity, because targets can be composed in ways runlevels never could.

Change the default through systemd, never by editing the symlink yourself. systemctl set-default rewrites /etc/systemd/system/default.target atomically and validates that the target exists; a hand-edited symlink that points at a typo or a nonexistent unit drops the machine to emergency mode on the next boot with no obvious cause.

# read and change the boot default
systemctl get-default
multi-user.target

systemctl set-default multi-user.target
# Removed /etc/systemd/system/default.target.
# Created symlink ... -> /usr/lib/systemd/system/multi-user.target.

Boot Sequence and Ordering

Below the target you choose sits a fixed scaffold every boot climbs through. sysinit.target gathers early setup — mounting filesystems, setting up swap, applying sysctl, starting udev. basic.target follows once sockets, timers, and paths are ready. Only after basic.target is reached do the bulk of system services start, and finally the chosen default.target caps the chain. Each layer is ordered After the one below it, which is how parallel startup stays correct without a hand-written sequence.

The point people miss is that Wants/Requires and Before/After are independent. Dependency settings decide whether a unit is pulled in; ordering settings decide when it runs relative to others. A service can Want the network and still race ahead of it unless it also declares After=network-online.target and that target is wanted. Forgetting the ordering half is the classic reason a daemon starts before its database socket exists and crashes on the first boot after a reboot.

# a service that must wait for the network to be usable
[Unit]
Wants=network-online.target
After=network-online.target

[Service]
ExecStart=/usr/bin/myapp --listen 0.0.0.0:8080

Isolating and Rescue

systemctl isolate switches the running system to a different target now, starting everything that target wants and stopping everything outside it. rescue.target brings the machine to a single-user state with a root shell and local filesystems mounted but no general services or networking; emergency.target goes further, starting an emergency shell on the main console with no other services or mounts pulled in — the root filesystem stays in whatever state it was reached in (read-only if you booted with ro, since the target does no remounting of its own). Both are recovery states, and isolate-ing into them on a remote box without console access cuts your own connection — there is no networking and no sshd left running.

The safer entry point for recovery is the boot loader. Append systemd.unit=rescue.target (or emergency.target) to the kernel command line in GRUB, and the machine boots straight into that state from power-on rather than transitioning a live system. This is the reliable way to fix a box that fails to reach multi-user.target — a broken fstab entry, a service that hangs the boot — because you get a shell before the failing units are ever started.

# switch a local machine to rescue mode interactively
systemctl isolate rescue.target

# or boot into it from GRUB by editing the linux line:
linux /vmlinuz ... ro systemd.unit=rescue.target

Boot Performance

systemd-analyze tells you where boot time went. The bare command prints the split between the boot phases — kernel, initrd, and userspace, plus firmware and loader on UEFI systems; systemd-analyze blame lists every unit sorted by how long it took to initialize. Blame is the first place to look when a server takes ninety seconds to come up, but read it carefully — a unit at the top of the list is not necessarily on the boot's critical path, it may have been running in parallel while nothing waited on it.

systemd-analyze critical-chain is the answer to that. It walks the ordering graph and shows the actual longest dependency path to a target, with the time each step contributed and the timestamp it finished. That is what you optimize: a unit on the critical chain delays everything after it, while a slow unit off the chain costs nothing. Common offenders are NetworkManager-wait-online or systemd-networkd-wait-online blocking on an interface that will never get an address, and a service whose After= chain is longer than it needs to be.

# overall split, then per-unit, then the real critical path
systemd-analyze
Startup finished in 3.1s (kernel) + 8.7s (userspace) = 11.8s

systemd-analyze blame
systemd-analyze critical-chain multi-user.target
Common Mistakes
  • Treating targets as runlevels one-to-one — expecting exactly seven numbered states when targets are composable graph nodes, so runlevel3.target is a compatibility alias for multi-user.target, not a distinct level.
  • Editing the default.target symlink by hand. Point it at a typo or a unit that does not exist and the next boot drops to emergency mode with no obvious cause; systemctl set-default validates the target first.
  • systemctl isolate-ing rescue.target or emergency.target on a remote server — it stops networking and sshd, cutting your own session, and you need console access to get back in.
  • Confusing Wants/Requires with Before/After. Declaring a dependency on the network without also declaring After=network-online.target lets the service race ahead and crash before the socket exists.
  • Reading systemd-analyze blame as the boot critical path. A unit at the top may have run in parallel off the critical chain, so "fixing" it changes nothing — critical-chain shows what actually delays boot.
  • Leaving graphical.target as the default on a headless server, dragging in a display manager and X libraries that consume memory and widen the attack surface for no benefit.
Best Practices
  • Change the boot default with systemctl set-default, never by hand-editing the symlink — it validates the target and writes the link atomically.
  • Keep multi-user.target as the default on every server; reserve graphical.target for machines that genuinely need a desktop.
  • Test a target switch with systemctl isolate on a console-reachable machine before committing it with set-default, so a mistake is one reboot away from recovery.
  • Debug slow boots with systemd-analyze critical-chain, not blame alone — optimize only units that sit on the real dependency path to your target.
  • Recover an unbootable machine by appending systemd.unit=rescue.target to the kernel line in GRUB, so you get a shell before the failing units run.
  • Pair every Wants=network-online.target with After=network-online.target in service units so ordering matches the dependency and the service never races the network.
Comparable toolsSysV runlevels — the numbered 0–6 states (/etc/inittab, rc.d scripts) that targets replaced; sequential, not a dependency graphUpstart — Ubuntu's pre-systemd event-driven init, where job state was triggered by emitted events rather than a resolved target graphWindows services.msc — per-service startup types (Automatic, Manual, Disabled) with dependency lists, the closest mainstream contrast to enabling a service into a target

Knowledge Check

A service declares Wants=network-online.target but no ordering directive, and it crashes on boot because the network is not up yet. What is missing?

  • After=network-online.targetWants pulls the target in but does not order the service after it, so the service can still start first
  • A Requires= directive used in place of Wants=, because only Requires actually makes the unit wait for the named dependency to finish starting first
  • An explicit enable of network-online.target, because targets do not ever activate unless they are enabled by hand
  • A higher numbered runlevel set in the unit's [Install] section in order to delay when it starts

You need to recover a server that hangs before reaching multi-user.target. Which approach is safest?

  • Append systemd.unit=rescue.target to the kernel line in GRUB so the machine boots into a shell before the failing units start
  • SSH into the box and run systemctl isolate emergency.target to drop the running services and then calmly inspect the broken system
  • Edit the default.target symlink to point at rescue.target from another machine's recovery mount
  • Boot normally and then run systemd-analyze blame to find the one unit that is hanging boot

Why is systemd-analyze critical-chain more useful than blame for cutting boot time?

  • It shows the longest dependency path to the target, so the units it lists actually delay boot, whereas a slow unit in blame may have run in parallel off the critical path
  • blame only measures the kernel-side time spent before the initramfs hands off, while critical-chain instead measures all of the userspace startup time that comes after it
  • critical-chain sorts the units it prints by peak memory usage rather than by their measured start duration
  • blame silently excludes every failed unit from its output, so it always undercounts the real total boot time

What is the relationship between runlevel3.target and multi-user.target on a modern systemd machine?

  • runlevel3.target is a compatibility alias (symlink) for multi-user.target, provided so old runlevel habits keep working
  • They are two independent targets that must both be reached separately, in strict sequence, during a normal boot
  • runlevel3.target is the kernel-level boot state, while multi-user.target is its entirely separate user-space equivalent
  • multi-user.target pulls in runlevel3.target the way graphical.target pulls in multi-user.target

You got correct