DEV Community

Cover image for Podman Quadlet Deep Dive: How a .container Becomes a systemd Service
Algo7
Algo7

Posted on

Podman Quadlet Deep Dive: How a .container Becomes a systemd Service

Introduction

If you have a simple container service that doesn't justify an orchestrator (k8s / Docker Swarm), but docker compose up -d feels too non-production, Podman Quadlet is worth a check. Quadlet lets you manage your container resources as systemd unit files; you write a declarative .container file, and systemd runs it like any other service.

That's probably all you will find if you don't take a deep dive into the official docs. This post investigates what actually happens when you drop that file in place. We'll trace how Podman turns a .container into a real systemd .service, where that generated unit lives, why it's transient, and why those mechanics dictate how you start it and keep it running.



Prerequisites

  • A Linux-based VM (this post uses Ubuntu)
  • Podman >=v4.4.0 installed
  • A container image to run

Today's Scope

There are many types of Quadlet files, but today we are only covering the rootless/user .container type.

Type of Quadlet Files

The above image is taken from the official doc


How It Works

A .container file looks like a systemd unit, but, well, it's not. The gap between "the file you write" and "the unit systemd runs" is our focus today, so let's walk it before reaching for copy-and-paste.

Quadlet Files to systemd Unit Files

Quadlet files use the same format as systemd unit files but they aren't valid units on their own. Keys such as Image= and PublishPort= are Podman specific. Podman uses a systemd unit generator to convert the Quadlet files into standard systemd .service files.

User Manager (rootless)

When you use systemctl --user, the per-user manager runs /usr/lib/systemd/user-generators/podman-user-generator, which converts your Quadlet files into .service units and drops them in $XDG_RUNTIME_DIR/systemd/generator/. Because this manager runs as your UID, the containers it launches are rootless.

System Manager (rootful)

When you use systemctl (no --user), the system manager (PID 1) runs /usr/lib/systemd/system-generators/podman-system-generator, with output in /run/systemd/generator/. Because this manager runs as root, the containers it launches are rootful.

Note: Both generators are symlinks to /usr/libexec/podman/quadlet (path may vary by distro)

Read more about systemd generator output directories


Creating Our Quadlet File

With the mechanics out of the way, let's create a .container Quadlet:

[Container]

# the container name shown when running podman ps
# Ref: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#containername
ContainerName=my-container

# the file that contains all the needed env vars
# Ref: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#environmentfile
EnvironmentFile=my-env-file

# The image to run
# Ref: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#image
Image=my-image:latest

# port mapping (hostPort:containerPort)
# note that rootless units can't bind to privilege ports
# Ref: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#id30
PublishPort=3000:3000/tcp

[Service]
# restart policy
Restart=always

[Install]
# starts on boot
WantedBy=default.target
Enter fullscreen mode Exit fullscreen mode

For the complete list of keys for .container unit files please refer to the official doc

For rootless/user units, the podman systemd generator reads from $XDG_CONFIG_HOME/containers/systemd/ or ~/.config/containers/systemd/ if $XDG_CONFIG_HOME is unset. We can save the above file as ~/.config/containers/systemd/my-service.container

Please refer to the official doc for the comprehensive list of rootless unit search paths.


Running the Service

Since it's a new unit, we need to tell our user systemd manager to reload. For a Quadlet unit, daemon-reload re-runs the generators (regenerating the .service from our .container) and reloads units from disk:

systemctl --user daemon-reload
Enter fullscreen mode Exit fullscreen mode

The --user flag points systemctl at the per-user systemd manager which is running as your UID, not the system manager (PID 1). Note this picks which manager runs the unit; it's a separate thing from whether the container itself is rootless.

Start the service:

systemctl --user start my-service
Enter fullscreen mode Exit fullscreen mode

Why systemctl start and not enable like normal systemd units? The systemd units Quadlet generates are transient; they live in $XDG_RUNTIME_DIR/systemd/generator/ (user) or /run/systemd/generator (system), and both are tmpfs. The generator regenerates them every time it runs (at boot and each daemon-reload). systemd won't let you systemctl enable a transient/generated unit, so you can't enable it the usual way. Instead, the generator reads the [Install] section from your .container and creates a symlink in $XDG_RUNTIME_DIR/systemd/generator/default.target.wants/ (user) or /run/systemd/generator/default.target.wants/ (system) as part of that same generation pass. That's why WantedBy=default.target is what makes it start on boot, not enable.

If you try systemctl --user enable my-service you will get

Failed to enable unit: Unit /run/user/<uid>/systemd/generator/my-service.service is transient or generated.
Enter fullscreen mode Exit fullscreen mode

systemd captures the service's stdout/stderr to the journal (via journald). To follow the logs:

journalctl --user -u my-service -f
Enter fullscreen mode Exit fullscreen mode

Now you can open $XDG_RUNTIME_DIR/systemd/generator/my-service.service to check the generated systemd unit file:

# Automatically generated by /usr/lib/systemd/user-generators/podman-user-generator
#
# my-service.container
[X-Container]
ContainerName=my-container
EnvironmentFile=my-env-file
Image=my-image:latest
PublishPort=3000:3000/tcp

[Service]
Restart=always
Environment=PODMAN_SYSTEMD_UNIT=%n
KillMode=mixed
ExecStop=/usr/bin/podman rm -v -f -i --cidfile=%t/%N.cid
ExecStopPost=-/usr/bin/podman rm -v -f -i --cidfile=%t/%N.cid
Delegate=yes
Type=notify
NotifyAccess=all
SyslogIdentifier=%N
ExecStart=/usr/bin/podman run --name=my-container --cidfile=%t/%N.cid --replace --rm --cgroups=split --sdnotify=conmon -d --publish 3000:3000/tcp --env-file /home/ubuntu/.config/containers/systemd/my-env-file my-image:latest

[Install]
WantedBy=default.target

[Unit]
SourcePath=/home/ubuntu/.config/containers/systemd/my-service.container
RequiresMountsFor=%t/containers
Enter fullscreen mode Exit fullscreen mode

You can see that an X- is prefixed to the Podman-specific [Container] section. That's systemd's way of saying ignore it.


The Big Gotcha

Here's the part that bites people on a remote VM: your systemd --user manager only exists while you have an active session. It starts on your first login and stops when your last session ends. When it does, your user services go with it. So if you start the service over SSH and then close the SSH session, the service dies with it.
To keep the user manager alive at boot with no session attached, you must enable user lingering:

# enable lingering for the current user
loginctl enable-linger

# verify
loginctl show-user "$USER" --property=Linger
Enter fullscreen mode Exit fullscreen mode

Lingering is recorded under /var/lib/systemd/linger/ and survives reboots. Note that lingering doesn't make you "logged in"; it only controls whether your per-user systemd --user manager runs without an active session. For the service to actually come up on boot you still need the [Install] WantedBy=default.target section in your Quadlet file as noted in Running the Service and the official doc

For details, see the loginctl docs.

Top comments (0)