How to run Podman containers under Systemd with Quadlet

Quadlet is a free and open source tool written in C which let us create and run Podman containers under Systemd. The tool let us declare containers, volumes, networks, and their relationships, using dedicated Systemd units.

In this tutorial we learn how to use Quadlet to create Podman containers, networks and volumes, and how to create multi-container stacks.

In this tutorial you will learn:

  • How to create containers, volumes and networks with the corresponding Systemd units
  • How to create a multi-container stacks using Quadlet
How to run Podman containers under Systemd with Quadlet
How to run Podman containers under Systemd with Quadlet
Category Requirements, Conventions or Software Version Used
System Distribution-agnostic
Software Podman
Other None
Conventions # – requires given linux-commands to be executed with root privileges either directly as a root user or by use of sudo command
$ – requires given linux-commands to be executed as a regular non-privileged user

A basic example: creating a MariaDB container

In this first, basic example, we define a unit for a MariaDB database server. Here is what it looks like:

[Unit]
Description=MariaDB container

[Container]
Image=docker.io/mariadb:latest
Environment=MYSQL_ROOT_PASSWORD=rootpassword
Environment=MYSQL_USER=testuser
Environment=MYSQL_PASSWORD=testpassword
Environment=MYSQL_DATABASE=testdb

[Install]
WantedBy=multi-user.target

With this few lines, we defined a container based on the latest official MariaDB image, available on Dockerhub. As you can see, a “.container” is a dedicated type of Systemd unit. What is unique to this type of unit, is the “Container” section, in which we can specify options which are the equivalents of those we can pass to the Podman command line. The only required option in the “Container” section, is Image, to specify the base image for the container.



If we want the container to run as root, we save the file under the /etc/containers/systemd directory; to run it as our own unprivileged user,  instead, we save it under ~/.config/containers/systemd. In this example, we will use the latter approach, and save our “.container” unit as ~/.config/containers/systemd/mariadb-service.container. To let systemd generate a service based on the container unit, we run the following command:

$ systemctl --user daemon-reload

After we run the command, we can take a look at the generated service by running:

$ systemctl --user cat mariadb-service

To start the container, just as any other service, we can run:

$ systemctl --user start mariadb-service

The first time we launch it, the service could take a while to start, if the container base image doesn’t exist on our system. We can use the podman command line to verify the status of the container:

$ podman ps

The command returns the following output, which confirms the container to be up and running:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
151226b10a1c docker.io/library/mariadb:latest mariadbd 59 seconds ago Up About a minute systemd-mariadb-service

As you may have noticed, by default, the container is named after the “.container” unit file plus the “systemd-” prefix (“systemd-mariadb-service”, in this case). To explicitly assign a name to the container, we can, however, pass it as the value of the ContainerName option inside the “[Container]” section.

Creating a “volume” unit

We use volumes to persist containers data. In this case, we want our database data to persist even if we drop the systemd-mariadb-service container. We define a named volume in the corresponding  “.volume” unit. In the example below, we provide just a description for the volume. Volume-specific options can be specified under the [Volume] stanza:

[Unit]
Description=MariaDB Volume

[Volume]



We save the unit as ~/.config/containers/systemd/mariadb-volume.volume. To instruct the MariaDB container to use the volume, we use the Volume option inside the [Container] section of the “.container” unit, and we map the volume to the /var/lib/mysql directory inside the container:

[Unit]
Description=MariaDB container

[Container]
Image=docker.io/mariadb:latest
Environment=MYSQL_ROOT_PASSWORD=rootpassword
Environment=MYSQL_USER=testuser
Environment=MYSQL_PASSWORD=testpassword
Environment=MYSQL_DATABASE=testdb
Volume=mariadb-volume.volume:/var/lib/mysql

[Install]
WantedBy=multi-user.target

When we use a named volume, Systemd automatically assigns a dependency on it in the service unit it generates for our container. To let systemd regenerate the required services, once again, we use the daemon-reload command. Once we (re)start the MariaDB container, the volume is created automatically, as we can verify by using the podman command-line utility:

$ podman volume ls

As expected, our volume appears in the list generated by the command:

DRIVER        VOLUME NAME
local         systemd-mariadb-volume

We can also verify the volume is mounted on /var/lib/mysql inside the container by using the podman inspect command, passing the name of the container as argument, and taking a look at the “Mounts” section:

$ podman inspect systemd-mariadb-service

Here is the relevant section of the output:

"Mounts": [
     {
         "Type": "volume",
         "Name": "systemd-mariadb-volume",
         "Source": "/home/doc/.local/share/containers/storage/volumes/systemd-mariadb-volume/_data",
         "Destination": "/var/lib/mysql",
         "Driver": "local",
         "Mode": "",
         "Options": [
              "nosuid",
              "nodev",
              "rbind"
         ],
         "RW": true,
         "Propagation": "rprivate"
     }
],

Using bind-mounts

What if we want to use bind-mounts instead of named volumes? Bind-mounts are useful during development, since they let us mount host files inside the container. The downside of using bind-mounts is that containers become dependent on the host filesystem. To use a bind-mount inside a “.container” unit, we just specify the relative or absolute path of the host file we want to mount. Suppose, for example, we want to bind-mount the /var/lib/mysql directory inside the container, to the ~/mysql directory on the host. Here is what we could write in the “.container” unit:

[Unit]
Description=MariaDB container

[Container]
Image=docker.io/mariadb:latest
Environment=MYSQL_ROOT_PASSWORD=rootpassword
Environment=MYSQL_USER=testuser
Environment=MYSQL_PASSWORD=testpassword
Environment=MYSQL_DATABASE=testdb
Volume=%h/mysql:/var/lib/mysql

[Install]
WantedBy=multi-user.target

In the example above, you can notice we used the %h placeholder, which is automatically expanded by Systemd to the absolute path of our own HOME directory. It is also possible to use relative paths, which are resolved relatively to the position of the unit file.

Creating Networks

To create a stack composed of multiple containers, similarly to what we do with docker-compose, we must put containers in the same network, so that they can talk to each other. To create a network with Quadlet, we can use units with the “.network” extension.



As happens for containers and volumes, by default, networks are named after the unit file in which they are defined, plus the “systemd-” prefix. We can explicitly provide a name, or any other network-specific option, under the [Network] section of the file. In the example below, we specify the subnet and gateway address for the network (this is the equivalent of running podman with the --subnet 192.168.30.0/24 and --gateway 192.168.30.1 options):

[Unit]
Description=MariaDB Network

[Network]
Subnet=192.168.30.0/24
Gateway=192.168.30.1

We save the file as ~/.config/containers/systemd/mariadb.network. To “put” a container in a specific network, we use the Network option inside the “Container” section of its container unit. The MariaDB container we previously defined, becomes:

[Unit]
Description=MariaDB container

[Container]
Image=docker.io/mariadb:latest
Environment=MYSQL_ROOT_PASSWORD=rootpassword
Environment=MYSQL_USER=testuser
Environment=MYSQL_PASSWORD=testpassword
Environment=MYSQL_DATABASE=testdb
Volume=mariadb-volume.volume:/var/lib/mysql
Network=mariadb.network

Creating a multi-container stack

We have a container running our MariaDB server. To add an easy way to manage our database via a web interface, we can create a container based on phpMyAdmin. To make the two containers able to talk to each other, we must put them in the same network.

Since the phpMyAdmin container, in order to work correctly, requires the MariaDB server to be active and running, we can declare this dependency as we would do for any other Systemd service, with the Requires and After options in the “[Unit]” section. The former establishes a hard-dependency on the service passed as its value; the latter ensures our service starts after it. By the way, if you are not familiar with Systemd, you can take a look at our tutorial on how to create a Systemd service. Here is how the container unit for phpMyAdmin looks like:

[Unit]
Description=phpMyAdmin container
Requires=mariadb-service.service
After=mariadb-service.service
 
[Container]
Image=docker.io/phpmyadmin:latest
Network=mariadb.network
Environment=PMA_HOST=systemd-mariadb-service
PublishPort=8080:80

[Install]
WantedBy=multi-user.target

In the example above, we also used the PublishPort option, which is the equivalent of podman -p  (--publish): it is used to map a host port to a container port. In this case, we mapped port 8080 on the host system, to port 80 inside the container.

We also provided a value for the PMA_HOST environment variable, via the Environment option: this is used to specify the address of the database server (in this case we used the MariaDB container name, which is resolved to the actual address). Notice that we used the name of the MariaDB container as the value of the environment variable, not the name of the “.container” unit. To start our new setup, once again, we run:

$ systemd --user daemon-reload

Then, we can start the generated “phpmyadmin-service” service, which, in turn, runs the phpMyAdmin container. Since it has a hard dependency on the MariaDB one, the latter will automatically start too. We should be able to reach phpMyadmin at the “localhost:8080” address. To login, we can use the credentials specified in the MariaDB container unit:

phpMyAdmin login page
phpMyAdmin login page

Closing thoughts

In this tutorial we learned how to create and run Podman containers, volumes and networks under Systemd using Quadlet. Instead of defining multi-containers stacks in a single file, like we do when using docker-compose, with Quadlet, we define containers, volumes and networks using dedicated Systemd units.



Comments and Discussions
Linux Forum