Right, so you’ve escaped the SysV init system, that dusty old museum of shell scripts, and you’ve landed here in systemd. Good move. But now you’re faced with its new organizational principle: targets. Forget runlevels; they’re gone. systemd targets are their spiritual successor, but they’re far more powerful and, frankly, less obtuse. Think of a target not as a rigid state, but as a logical grouping of units (services, sockets, etc.) that you want to activate together to achieve a specific purpose. It’s less about a numbered “level” and more about a named “goal.”

How Targets Relate to Runlevels (The Map for Refugees)

To keep a foot in both worlds and stop seasoned sysadmins from rioting, systemd provides compatibility. It creates symbolic links from the old runlevel concepts (like multi-user.target) to the new target names. You can even use the old telinit 3 command, and it will politely translate it to systemctl isolate multi-user.target for you behind the curtains. It’s a nice gesture, but you should really learn the new syntax. It’s like still using the ’90s-era linuxconf tool; it works, but everyone knows you’re not really trying.

You can see this mapping for yourself. Those old familiar friends like runlevel3.target are just symlinks to the real systemd targets.

ls -la /lib/systemd/system/runlevel*.target

You’ll see something like:

lrwxrwxrwx 1 root root 15 Apr  5  2022 /lib/systemd/system/runlevel3.target -> multi-user.target

The Important Built-in Targets

systemd comes with a set of built-in targets that form the backbone of your system’s operation. Here are the big ones:

  • poweroff.target & reboot.target: These do exactly what they say on the tin. They’re for shutting down and rebooting. You usually won’t isolate these manually.
  • rescue.target: This is roughly equivalent to single-user mode. It mounts the bare minimum filesystems and gives you a root shell, which is invaluable when you’ve borked something and need to fix it. It’s your “oh crap” mode.
  • multi-user.target: This is your workhorse, the equivalent of runlevels 3 and 4. This is where your system has all network services running but no graphical interface. Your servers live here.
  • graphical.target: This is runlevel 5. It’s multi-user.target plus a graphical login screen (gdm, sddm, etc.). Your desktop or workstation boots to this by default.

You can see what your system is currently running and what it wants to run by default with:

systemctl get-default

Isolating Targets: Changing System States

This is the key operation. You don’t “set” a runlevel; you “isolate” a target. The isolate command is brutally literal. It stops all units not required by the new target and starts all units required by it. It’s a surgical strike, not a gentle nudge.

Warning: Do not isolate a target unless you are certain it includes all the essential units your system needs to, you know, continue existing. Isolating rescue.target on a remote machine will kill your SSH session and leave you with a very quiet, very offline system. You’ve been warned.

To switch to multi-user mode (killing your GUI in the process if you have one):

sudo systemctl isolate multi-user.target

To switch back to graphical:

sudo systemctl isolate graphical.target

Setting the Default Target

You don’t want to isolate your target at every boot. You want to tell systemd what to aim for on startup. This is done by setting a symbolic link. The set-default command just manages this link for you.

To make your server boot to the pure, efficient, text-only glory of multi-user mode:

sudo systemctl set-default multi-user.target

To tell your desktop to boot to a GUI:

sudo systemctl set-default graphical.target

Run systemctl get-default again to confirm your change.

How Dependencies and “Wants” Actually Work

This is where targets get clever. A target file (a .target unit) doesn’t actually do anything itself. It’s just a basket. Its power comes from its dependencies, defined by directives like Wants=, Requires=, After=, and Conflicts=.

When you isolate graphical.target, systemd looks at that target’s unit file and sees:

Wants=display-manager.service
After=display-manager.service multi-user.target
...

This tells systemd: “Okay, to achieve this graphical goal, you want the display manager service, and you need to make sure it happens after multi-user.target and itself are up.” It then resolves all the dependencies recursively, building a tree of what to start and in what order. This declarative approach is why it’s so much more robust than the procedural “run this script, then maybe that one” of SysV init.

Creating Your Own Custom Target

Why would you? Imagine you have a set of services for a specific application—say, a web backend, a cache, and a queue worker. You might want to start them all together for development but keep them disabled on production. You can create a custom target that Wants= all those services.

First, create the target unit file:

sudo editor /etc/systemd/system/my-app-environment.target

Fill it with the basics:

[Unit]
Description=My Awesome App Environment
Documentation=man:systemd.special(7)
After=multi-user.target network.target
AllowIsolate=yes

The AllowIsolate=yes is crucial; it lets you actually isolate this target. Without it, you can only use it as a dependency.

Now, make your services WantedBy= this new target. You can either modify the service files directly or, better yet, create drop-in snippets. But the simplest way is often to just have your target Wants= them. It’s a matter of design philosophy.

After creating any new unit, you must tell systemd to reload its configuration:

sudo systemctl daemon-reload

Now you can isolate my-app-environment.target and bring your entire app stack up or down with a single, clean command. That’s the real power systemd targets give you: the ability to manage the state of your machine not as a numbered mystery, but as a named, logical concept.