systemd by example
Part 4: Installing units
This article is part of the series systemd by example. The following articles are available.
Part 4: Installing units (this article)
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
After=, and requirement dependencies, with the most common directives
Requires=. In this post, we are concerned with the latter dependency type.
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
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 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
Let’s use the 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
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
default.target.requires) and add a symlink to
hello-world.service. Let’s see this in practice.
After starting the system, execute
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
default.target.wants directory now contains a symlink to
Finally, we tell systemd to reload its configuration.
If we now execute
again, we see
default.target ● ├─hello-world.service ● ├─systemd-journald.service ● └─sysinit.target
hello-world.service is indeed added as a dependency of
We can see this also by looking at systemd’s in-memory representation of
This shows (among other things)
Note that this only affects the in-memory representation. The unit file of
default.target itself was not changed. We can confirm this with
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
Requires= without touching the unit files at all. This mechanism is the basis for the
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.
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
hello-world.service again, we can add an
[Install] section as follows.
We still need to tell systemd to do the actual installation. After starting the system, execute
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
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
(or activate it manually with
systemctl start hello-world.service).
The service can be disabled again with
which will remove the symlinks.
Examples from the real world
[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
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
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).
There are three different ways to define
Requires= dependencies between two services
b.service (or any other units).
- The unit file of
a.servicehas a directive
- The directory
b.service.wantscontains a symlink to
- The unit file of
b.servicehas a directive
[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 Twitter @crepels for updates on new blog posts.