May 24, 2022

systemd by example

Part 4: Installing units

Series overview

This article is part of the series systemd by example. The following articles are available.

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.

Follow along on the systemd playground Example 1: 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!"
hello-world.service

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

Follow along on the systemd playground Example 2: 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
hello-world.service

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).

  1. The unit file of a.service has a directive Wants=b.service in its [Unit] section.
  2. The directory b.service.wants contains a symlink to a.service.
  3. The unit file of b.service has a directive WantedBy=a.service in its [Install] section and has been enabled through systemctl 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.