systemd by example
Part 4: Installing units
Series overview
This article is part of the series systemd by example. The following articles are available.
Part 4: Installing units (this article)
Introduction
This is the fourth article in a series trying to understand systemd by creating small containerized examples. In Part 1, we created a minimal systemd setup in a container. In Part 2 we took a close look at systemd’s dependency management. In Part 3 we saw the basics of services and how to define them. In this post, we will see another way to add dependencies for units. This technique is most commonly used when adding new units to the system that should be activated during bootup.
Recap of dependencies
Let’s briefly recap systemd dependencies (see Part 2: Dependencies for more details). There are two types of dependencies: ordering dependencies, specified with the directives Before=
and After=
, and requirement dependencies, with the most common directives Wants=
and Requires=
. In this post, we are concerned with the latter dependency type.
If a.service
has a requirement dependency on b.service
, then whenever a.service
is activated, so is b.service
. We have used this several times already. For example, in the minimal setup of Part 1, default.target
has a Requires=
dependency on systemd-journald.service
, so when the system boots, default.target
gets activated and with it the journald service. Similarly, halt.target
has a Requires=
dependency on halt.service
, so when the system is shut down, systemd activates halt.target
, which causes the execution of halt.service
.
Now assume that we want to add a new service to our system, for example a webserver that we want to start as soon as the system boots up. To do this, we first need to write a service unit file that describes how to start the webserver, and then we need to add a requirement dependency to default.target
to ensure that the service is activated on system start. This means that the information about the service is spread across two different places: the service file contains how the webserver is started, and default.target
specifies when the service started. To make matters worse, the information is usually contained in different directories: default.target
lives in /lib/systemd/system
(the directory for units installed by the distribution), whereas our service unit should live in /etc/systemd/system
(the directory for units installed by the system administrator).
(Side note: On the systemd playground, all units live in /lib/systemd/system
for simplicity. On a real system you should stick to the separation.)
If we imagine systemd units as characters with their own will and we take directives literally, it’s not default.target
that requires our service. In fact, default.target
couldn’t care less whether the service is started or not, it works just fine without the service. (Note that this is different for halt.target
and halt.service
. halt.target
really requires halt.service
to perform the actual halting; without it, the target would do nothing and would be fairly pointless.) It’s rather the other way around: our service would like to be started (or required) by default.target
. systemd allows us to specify exactly this in an [Install]
section of the service unit. This enables us to keep all information in one place.
Requirement dependencies via symlinks
To better understand how installing services works, let’s first take a detour and take a look at a different way to specify a Wants=
or Requires=
relationship.
Let’s use the service
[Service]
ExecStart=echo "Hello world!"
We want to add this service as a requirement dependency of default.target
. We can do this by adding a Requires=hello-world.service
line to default.target
as we saw in Part 2: Dependencies. But for Wants=
and Requires=
, there’s an alternative to define the dependencies. Instead of adding the directive to the default.target
unit file, we can create a directory /lib/systemd/system/default.target.wants
(or default.target.requires
) and add a symlink to hello-world.service
. Let’s see this in practice.
After starting the system, execute
systemctl list-dependencies default.target
This returns
default.target
● ├─systemd-journald.service
● └─sysinit.target
which confirms that hello-world.service
is currently not a dependency of default.target
. Now we create the directory and symlink with
mkdir /lib/systemd/system/default.target.wants
and
ln -s /lib/systemd/system/hello-world.service /lib/systemd/system/default.target.wants/
The default.target.wants
directory now contains a symlink to hello-world.service
.
Finally, we tell systemd to reload its configuration.
systemctl daemon-reload
If we now execute
systemctl list-dependencies default.target
again, we see
default.target
● ├─hello-world.service
● ├─systemd-journald.service
● └─sysinit.target
so hello-world.service
is indeed added as a dependency of default.target
.
We can see this also by looking at systemd’s in-memory representation of default.target
with
systemctl show default.target
This shows (among other things)
Wants=hello-world.service
Note that this only affects the in-memory representation. The unit file of default.target
itself was not changed. We can confirm this with
systemctl cat default.target
which shows our original file
# /lib/systemd/system/default.target
[Unit]
Description=A minimal default target
Requires=systemd-journald.service sysinit.target
So that’s it. By simply adding a symlink to a special directory we can set up requirement dependencies of type Wants=
and Requires=
without touching the unit files at all. This mechanism is the basis for the [Install]
section.
One important thing to note is that we only set up the dependencies. This didn’t actually start hello-world.service
(check the logs to verify that there is no Hello world
output). The dependency says that whenever default.target
is activated, hello-world.service
is activated alongside it. But when we set up the dependency, default.target
was already active. So hello-world.service
will only be activated once default.target
is activated again, usually on the next boot.
Using an [Install]
section
Adding the symlinks manually like we just did is not what we would do in practice. Instead, we define an [Install]
section in the unit file. This section can only contain a handful of directives; among those are WantedBy=
and RequiredBy=
. Using hello-world.service
again, we can add an [Install]
section as follows.
[Service]
ExecStart=echo "Hello world!"
[Install]
WantedBy=default.target
We still need to tell systemd to do the actual installation. After starting the system, execute
systemctl enable hello-world.service
which prints
Created symlink /etc/systemd/system/default.target.wants/hello-world.service → /lib/systemd/system/hello-world.service.
This did automatically what we did manually above (the symlink is created under /etc/systemd/system
instead of /lib/systemd/system
, but the behavior is the same). The enable
command also automatically reloaded the daemon, so now hello-world.service
is a dependency of default.target
.
Note that as in the manual process, this only set up the dependency; it did not activate hello-world.service
. This will only happen once default.target
is activated again. If we also want to activate hello-world.service
at the same time, we can use the --now
flag:
systemctl enable hello-world.service --now
(or activate it manually with systemctl start hello-world.service
).
The service can be disabled again with
systemctl disable hello-world.service
which will remove the symlinks.
Examples from the real world
Using an [Install]
section is the standard way to set up dependencies for units that are not part of the core systemd setup. For example, if we install NGINX via the Ubuntu package manager, it will create a file /lib/systemd/system/nginx.service
with an install section
[Install]
WantedBy=multi-user.target
The package manager will also automatically enable the service. The same is true if we install PostgreSQL, ssh, or docker.
(Note that the target here is multi-user.target
, whereas in our example we always used default.target
. We did this because the systemd playground uses an intentionally minimal systemd setup which doesn’t have a multi-user.target
. But on a real system, default.target
is a symlink to a different target. On a desktop system it usually points at graphical.target
(which in turn depends on multi-user.target
) which starts the system with a display manager. But it could also be changed to point at rescue.target
, which only brings up the most basic system setup. Adding a dependency on default.target
would mean that NGINX is always started on boot-up, regardless of where default.target
points to, which is likely not what we want.)
In our example, we installed a service as a dependency of a target. But just as we can add dependencies between any two systemd units as we saw in Part 2: Dependencies, we can add [Install]
sections in any unit file and reference any other unit file.
For example, on my system gpu-manager.service
has an [Install]
section with a WantedBy=display-manager.service
; so gpu-manager.service
is activated whenever display-manager.service
is activated. Another example is docker.socket
, which is WantedBy=socket.target
(we will see more about socket units in a future post).
Simulating a reboot on the systemd playground
As mentioned above, enabling a unit only sets up the dependencies and does not activate the unit. In case of default.target
, the unit is activated on the next boot. But on the systemd playground there is no “next boot”; the system is destroyed as soon as it is stopped. To be able to simulate the behavior, whenever a unit has an [Install]
section, the UI shows a check mark symbol next to the unit file. When this is clicked, the unit is enabled before the system is started (by executing systemctl enable
in the Dockerfile).
Conclusion
There are three different ways to define Wants=
and Requires=
dependencies between two services a.service
and b.service
(or any other units).
- The unit file of
a.service
has a directiveWants=b.service
in its[Unit]
section. - The directory
b.service.wants
contains a symlink toa.service
. - The unit file of
b.service
has a directiveWantedBy=a.service
in its[Install]
section and has been enabled throughsystemctl enable b.service
.
From systemd’s point of view, all three approaches are eventually equivalent: the in-memory representations of the unit files are the same in each case, as can be verified with systemctl show a.service
.
As a user, we will most likely use the last option when adding a new unit. It allows us to keep all information about the unit in one file, and it allows us to enable or disable the unit without editing any unit files.
—Written by Sebastian Jambor. Follow me on Mastodon @crepels@mastodon.social for updates on new blog posts.