Overview Link to heading

This brief tutorial is best viewed as a sister blog post from the official Red Hat blog here. I wanted to introduce quadlets in a manner that more traditional system administrators may be able to parse, and to show that you don’t need to drink the entire Kubernetes cup to find useful paradigms in containers and cloud-native technology.

This gives administrators the ability to easily deploy containers in a declarative, centralized way that is local to a server, easily backed up, and easily managed using pre-existing tools (namely systemd). This turns running an entire application, and in some cases several applications, as simple as writing a .container file and starting a service.

So, here’s how to run Actual (open source budgeting software) as a rootless container orchestrated by systemd. We’ll be able to stop, start, view the status, and check the logs of the container from systemd.

Rootless Prerequisites & Assumptions Link to heading

  • This guide is running on RHEL 9.6 with Podman installed. Other linux distributions should work with minor tweaks.
  • A user to run the container. In my example, the user is nqnz
  • SELinux is set to enforcing.

Step 1 - The Container File Link to heading

Create a directory, in the $HOME of the user, to house systemd containers -

mkdir -p ~/.config/containers/systemd

Next we create a .container file in that directory, see below for the actual.container that I’ll be using

[Unit]
Description=Actual Budget Server (rootless, quadlet)
Wants=network-online.target
After=network-online.target

[Container]
ContainerName=actual
Image=ghcr.io/actualbudget/actual:latest
PublishPort=5006:5006
Volume=/tank/containers/actual/data:/data:Z

AutoUpdate=registry

[Service]
Restart=always
TimeoutStartSec=900

[Install]
WantedBy=default.target

.container File Breakdown Link to heading

[Unit] - Simple stanza where we give a description and details prerequisites for the container to start, along with when the service should run. In this case, after the network stack has started. For most of my containers, this is a reasonable default as most of my applications are serving content/services over the network.

[Container] - The image here is the public Github registry image, and we tell systemd to always pull down a new version. PublishPort ensures we can actually reach the service after its started by forwarding traffic from the host’s port (host:container), and the Volume is a user-owned directory on my system that we will let systemd (on behalf of my rootless user) create any container data. This volume information is largely gleaned from vendor documentation here

[Service] - This tells systemd to always restart the service on reboot or if it fails, given a reasonable timeout. Install - This actually tells systemd to ‘install’ the service at boot. This is what keeps our .service file persistent. More details on this here.

Step 2 - The Service File Link to heading

After we have a .container file, run the following -

systemctl --user daemon-reload

this will create a .service file from the .container file we made

systemctl --user start actual.service

starts the service

systemctl --user status actual.service

ensures the container has started sucessfully

Note: We don’t enable the .service files since Quadlets are referred to as “transient”.

Step 3 - Persistence Link to heading

Enable lingering. This functions like systemd is impersonating a login for the user, enabling us to create services that run under an account that would normally have to be logged in to run.

loginctl enable-linger nqnz

Step 4 - Firewall Rules Link to heading

sudo firewall-cmd --add-port=5006/tcp --permanent
sudo firewall-cmd --reload

Out of Scope Link to heading

User-namespace mapping can be a real pain, especially when you bind-mount host directories that weren’t created with your rootless UID/GID mapping in mind. I avoid most of that friction by keeping container state under directories owned by the rootless user, and (on SELinux systems) using :Z to apply a private label to bind mounts so they’re intended for a single container.

I will go over namespace mapping in a later post, but this should be enough scaffolding to get you creating rootless containers and playing around with running real services with Quadlets and systemd.