SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

systemd targets vs runlevels on a VPS

systemd targets replaced runlevels, and the old numbers survive only as aliases. See what a target really is, and how to change what your VPS boots into.

systemd targets replaced runlevels

systemd targets replaced runlevels, and the two are not the same idea under a new name. A runlevel was one number that described the whole machine. A target is a named unit that groups other units, and a running system sits in several targets at the same time. The numbered names survive, runlevel3.target and the rest, but each one is a symbolic link to a real target. They exist so that old scripts and old habits keep working.

That difference shows up the moment a boot goes wrong. A runlevel gave you a number and nothing else. A target gives you a set of units, so systemctl can tell you which member of the set failed.

What a target actually is

A target is a unit with nothing to run. It has a [Unit] section and no [Service] section, so there is no ExecStart= and no process behind it. What it holds is dependencies. Its job is to be a synchronisation point: a name that other units can order themselves against, and a handle you can start, stop or query.

systemctl cat multi-user.target
systemctl show -p Wants -p Requires -p After -p AllowIsolate multi-user.target

systemctl cat prints the unit file as it sits on disk. The file is short. You will find a description line, dependency lines such as Requires= and After=, a Conflicts= line that keeps this target apart from rescue mode, and AllowIsolate=yes. What you will not find is anything to execute. systemctl show prints the same information as resolved properties, one key=value per line, which is the form to parse in a script.

The dependency lines in that file are only half of the target. The other half lives in a directory beside it.

ls /usr/lib/systemd/system/multi-user.target.wants/
ls /etc/systemd/system/multi-user.target.wants/

Each directory holds symbolic links to unit files, and every link is a Wants= dependency added from outside the file. This is what enabling a service means. The SSH (secure shell) daemon unit, sshd.service on Rocky and Alma and ssh.service on Debian and Ubuntu, carries WantedBy=multi-user.target in its [Install] section. sudo systemctl enable ssh reads that line and creates the link under /etc/systemd/system/multi-user.target.wants/. Disabling deletes the link. The distribution's own links live under /usr/lib and yours live under /etc, so your changes survive a package upgrade.

Wants= and Requires= behave differently when a member fails, and neither of them says anything about order. Order comes from Before= and After=. The difference between Wants, Requires and the ordering directives is worth reading before you edit a target, because a target is made of nothing else.

What a runlevel was, and why it could not survive

SysV init (System V init, the boot system that systemd replaced) read /etc/inittab and kept one number. On Red Hat systems level 3 meant text mode with networking and level 5 meant a graphical login. On Debian, levels 2 through 5 were identical as shipped. Entering a level meant running the scripts in /etc/rc<N>.d/ whose names begin with K, then the ones whose names begin with S, in filename order, one process after another.

That design forced two things. The number was the entire description of the system, so a machine was in exactly one level and could never be in two. And the order came from file names, so the only way to say that A needs B was to give A a higher number, by hand, on every distribution.

systemd drops both constraints. Dependencies are declared inside each unit, so the manager works out the order itself and starts unrelated units in parallel. State becomes a set of active units instead of a scalar. The move from SysV init to systemd covers why that change happened. It also explains why the numbers could not simply be carried over: there is no single number left to carry.

systemd targets vs runlevels: the mapping is a shim

The numbered targets still ship. They are symbolic links.

ls -l /usr/lib/systemd/system/runlevel*.target

The links point where you would expect, with one surprise:

  • runlevel0.target points at poweroff.target
  • runlevel1.target points at rescue.target
  • runlevel2.target points at multi-user.target
  • runlevel3.target points at multi-user.target
  • runlevel4.target points at multi-user.target
  • runlevel5.target points at graphical.target
  • runlevel6.target points at reboot.target

Levels 2, 3 and 4 collapse into one target. A script that treats runlevel 2 as different from runlevel 3 now does the same thing in both cases, with no warning, because both names resolve to multi-user.target. The number is a redirect, and once it has been followed it carries no information of its own. That is the sense in which the mapping is a compatibility shim rather than a rename.

The runlevel command survives too, on systems that install the SysV compatibility package (systemd-sysv on Debian and Ubuntu). It reports a previous level and a current level, both of them a translation of the target the machine reached rather than a stored fact. Use it to satisfy an old script. Do not use it to answer the question of what is running.

Which targets a server operator meets

multi-user.target is the normal boot for a server. Filesystems are mounted and the network is configured. Every enabled service starts, including SSH, and you get text logins on the console and over the network. This is where a VPS should sit.

graphical.target is multi-user.target plus a display manager. It declares Requires=multi-user.target and After=multi-user.target, so it is a strict superset: everything in multi-user runs, then the login screen starts on top. On a headless server image with no desktop packages installed there is usually nothing for it to start, which is why a machine can boot into it and look completely normal.

rescue.target is the modern single-user mode. It pulls in sysinit.target first, so local filesystems are mounted and logging works, then it runs rescue.service to give you a root shell on the console, normally after asking for the root password. Ordinary services are not started, and that includes networking and SSH. A machine in rescue mode cannot be reached remotely.

emergency.target is one step below. It skips sysinit.target entirely. You get a shell, the root filesystem is often still mounted read only, and almost nothing else exists. This is where a boot lands when a filesystem check fails, or when /etc/fstab names a device that is not present. Run mount -o remount,rw / before you try to edit any file there.

sysinit.target, basic.target and network-online.target are the ones you never type but often reference. They are ordering handles for your own units. After=network-online.target means after the network is actually configured, while the more commonly copied network.target only means the networking software has been started. Choosing the wrong one produces a service that works when you restart it by hand and fails on every boot.

How do I check what my VPS boots into?

systemctl get-default
ls -l /etc/systemd/system/default.target
systemctl list-units --type=target
systemctl list-dependencies default.target

systemctl get-default prints one unit name and nothing else. That name is the boot default. It is read from the symbolic link /etc/systemd/system/default.target, so the ls line gives you the same answer plus the file it came from. When that link is absent, systemd falls back to /usr/lib/systemd/system/default.target, which upstream points at graphical.target. That fallback is why some server images report a graphical default while running no graphical software at all. Check the link rather than assuming the image set one.

systemctl list-units --type=target is the command that makes the difference concrete. The list is longer than most people expect: a dozen or more targets are active at the same moment, each with its own load and active state. A single number cannot express that. Add --all to include targets that are loaded but inactive, which is where the runlevel* aliases appear.

systemctl list-dependencies default.target prints an indented tree, one unit per line, expanding each target into its members. Read it downward and you can follow the boot: the default target wants multi-user, which wants basic, which wants sysinit, with the services each level pulls in hanging off the side. When a service you thought you enabled never runs, this tree is where you discover that nothing wants it. When it does appear in the tree and still fails, the unit's exit code and its journal lines tell you why.

How do I change the default target without locking myself out?

sudo systemctl set-default multi-user.target
systemctl get-default

set-default removes the old /etc/systemd/system/default.target link and writes a new one, reporting both the link it deleted and the link it created. The change applies at the next boot and does nothing to the running system, so a machine currently in graphical.target stays there until you reboot.

Two names must never become the default on a remote server: rescue.target and emergency.target. Neither starts the network, so neither starts SSH. Reboot into either one and the server is running, healthy, and unreachable. Recovery then needs your provider's serial or VNC (virtual network computing) console, or a rescue boot from their control panel. Confirm you have console access, and that you know the root password it will ask for, before you go near those two names.

From that console the fix is a one-time override on the kernel command line. Interrupt the boot loader, edit the kernel line, and append systemd.unit=multi-user.target. It applies to that boot only, which is enough to get a working system to run set-default on. The old SysV arguments are still honoured in the same place: a bare 3 on the kernel command line means systemd.unit=runlevel3.target, and 1, s or single means rescue.

systemctl isolate changes the running system instead of the next boot.

sudo systemctl isolate graphical.target

Isolating starts the named target and stops every unit the new target does not want. The second half is the dangerous half. sudo systemctl isolate rescue.target over SSH stops the SSH daemon, so your own session dies in the middle of the command and the machine does not come back until someone opens the console. Isolating toward a larger target is safe. Isolating toward a smaller one needs a console already open. Isolate also works only on targets that carry AllowIsolate=yes, and on anything else the command refuses and says so.

systemctl rescue and systemctl emergency are shorthand for isolating those two targets, and they carry the same warning. systemctl reboot and systemctl poweroff are the safe members of the family.

Why init 3 still appears to work

/sbin/init on a systemd machine is a symbolic link to the systemd binary. Confirm it with readlink -f /sbin/init.

The systemd manual page states the behaviour plainly: when the binary is invoked as init and it is not process ID 1, it executes telinit and passes the arguments through unchanged. telinit is itself a link to systemctl. So init 3 is three redirects deep, and what finally runs is systemctl isolate runlevel3.target, which is systemctl isolate multi-user.target, because that is where the alias points.

That is why the old command appears to work, and it is also where the illusion breaks. init 2 and init 4 do exactly the same thing as init 3, since all three aliases resolve to the same target. init 1 isolates rescue.target and ends your remote session. init 0 and init 6 map to poweroff and reboot. The numbers are accepted, translated, then discarded. Nothing in a running systemd system stores a level.

Those compatibility links come from a package, systemd-sysv on Debian and Ubuntu. A stripped down image can leave it out, and then init and telinit are simply missing while systemctl works normally. Write systemctl isolate multi-user.target in anything you plan to keep.

Writing a target of your own

Targets are cheap, and grouping your own services under one is a real use for them. Say a deployment has an API service and a queue worker, and you want to stop that pair for maintenance without touching the rest of the box.

[Unit]
Description=Application stack
Requires=multi-user.target
After=multi-user.target
AllowIsolate=no

Save it as /etc/systemd/system/app.target. Add WantedBy=app.target to the [Install] section of each service, run sudo systemctl daemon-reload, then sudo systemctl enable app-api.service. Enabling creates the link under /etc/systemd/system/app.target.wants/, and that link is what makes the service a member. Now sudo systemctl start app.target brings the group up.

One detail decides whether the other half works. WantedBy= starts members together, and it does not stop them together, so systemctl stop app.target leaves the services running. Add PartOf=app.target to each service and stop and restart on the target propagate to its members. A target changes grouping only. How systemd decides a service is ready is still governed by that service's own Type= setting, simple, exec, forking or notify.

FAQ

What is the systemd equivalent of runlevel 3?

multi-user.target. The name runlevel3.target still exists as a symbolic link to it, so both work. runlevel2.target and runlevel4.target point at the same target, so the distinction those numbers carried on SysV systems is gone. To make it the boot default, run sudo systemctl set-default multi-user.target. To switch the running system into it, run sudo systemctl isolate multi-user.target.

Does init 3 still work with systemd?

Yes, as a translation. /sbin/init is a link to the systemd binary, and when that binary runs as init without being process ID 1 it hands its arguments to telinit, which is a link to systemctl. The number maps to runlevel3.target, which is multi-user.target, and the result is an isolate. Those compatibility links come from the systemd-sysv package on Debian and Ubuntu, so a minimal image may not have them. Use systemctl isolate multi-user.target in scripts.

How do I stop my VPS booting into a graphical target?

Run systemctl get-default to see what is set, then sudo systemctl set-default multi-user.target. The setting is a symbolic link at /etc/systemd/system/default.target, and the change takes effect at the next boot. An image that never created that link reports graphical.target from the upstream fallback even with no desktop installed, so seeing that name does not by itself mean anything graphical is running.

What is the difference between rescue.target and emergency.target?

rescue.target pulls in sysinit.target first, so local filesystems are mounted and logging works before you reach a root shell. emergency.target skips that step: you get a shell with the root filesystem often still read only and almost nothing else running. Use rescue to repair a broken service, and emergency to repair a broken /etc/fstab or a filesystem that fails its check. Neither starts the network, so both need console access.

Can a Linux system be in more than one target at once?

Yes, and that is the core difference from runlevels. Run systemctl list-units --type=target and you will see many active together, because multi-user.target requires basic.target, which requires sysinit.target, and each of them stays active. A runlevel was one number and could hold one value. There is no single current target to read, only a default target for boot and a dependency graph that is currently satisfied.