Prelude
My personal website & blog is jfx.ac where I blog about once a month
That was a lie, where have I been? I got back from a holiday, moved to a MacBook at work, switched to GrapheneOS, caught the flu, and have been trying out Linux on some interesting mini PCs. Too many distractions, and I hope to get back into my blogging routine.
Introduction
For the past few years I’ve been using Podman containers to run most of my services. Before moving to containers, I would install services via a package manager, or build them from source, and run the binaries and manage the lifecycle with systemd
services.
The move to containers gave huge benefits. It made spinning services quick and easy, but managing the lifecycle of containers and performing mass upgrades for my many services across a couple of hosts felt cumbersome. I wanted to avoid heavy solutions and was yearning for something similar to the past. It was much easier to run a system upgrade and rely on systemd to manage my services.
I eventually started using a systemd template unit to manage my podman
containers in rootless fashion, and over the years landed on the template below:
# /etc/systemd/system/[email protected]
[Unit]
Description=%i rootless pod (podman-compose)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
RestartSec=20s
TimeoutStartSec=2min
User=%i
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/etc/podman/%i
ExecStart=/usr/local/bin/podman-compose up -d --remove-orphans
ExecStop=/usr/local/bin/podman-compose down
[Install]
WantedBy=default.target
The interface was familiar, and I could simply run systemctl status [email protected]
to manage my container for Home Assistant.
The term after the @
and before the type-suffix (in this case .service
) is the instance of the unit, and is substituted in the %i
variable in the definition. This substitution is used twice: once for the User=
which runs the container, and a second time in the WorkingDirectory=
path. This resulted in a simple and standardised way of managing rootless containers.
The unit file above worked OK, but occasionally after a reboot some containers would fail to start - which would annoyingly require manual intervention.
It always felt like I was forcing Podman into a systemd
shape it wasn’t designed for.
Enter Quadlets
Quadlets integrate Podman directly with systemd, making containers behave like first-class system services.
What are Quadlets?
The official documentation says:
Quadlet is a tool for running Podman containers under systemd in an optimal way by allowing containers to run under systemd in a declarative way.
The (now frozen) Quadlet repository gives a longer explanation:
Containers are often used in a cloud context, and they are then used in combination with an orchestrator like Kubernetes. They are also commonly used during development and testing to manually manage containers on an ad-hoc basis.
However, there are also use cases where you want some kind of automatic container management, but on a smaller, single-node scale, and often more tightly integrated with the rest of the system. Typical examples of this can be embedded or automotive use, where there is no system administrator, or disconnected or edge servers.
The recommended way to do this is to use systemd to orchestrate the containers, since this is an already running process manager, and since podman containers are just child processes. There are many documents that describe how to use podman with systemd directly, but the end result are generally large, hard to maintain systemd config files. And often the container setup isn’t optimal.
Basically, anywhere you want to run a containerised system service without requiring human intervention, it’s wise to use systemd
via Quadlets to manage your locally running Podman containers.
Because systemd carefully manages your container, you can rest easy that the lifecycle is tightly coupled to systemd.
Availability
Quadlets were first available to the public via their own repository on September 27th 2021. The project was then merged into Podman, and first released in Podman 4.4 on February 1st 2023.
Many of my hosts had Debian 12 bookworm installed, which shipped with Podman 4.3.1, missing the cutoff for Quadlet support. Debian 13 trixie, which was recently released on August 9th 2025 included Podman 5.4.2, which has support for Quadlets.
Getting started with Quadlets
An example of a Quadlet container is:
# [email protected]
[Unit]
Description=The sleep container
After=local-fs.target
[Container]
Image=registry.access.redhat.com/ubi9-minimal:latest
Exec=sleep %i
The definition uses the familiar systemd
unit file format, and the attributes under the [Container]
section match those of a Docker Compose file. The complete spec for the [Container]
section is available here: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container
A benefit of Quadlets is the containers have specific directories they will live, these are:
For system container files:
/run/containers/systemd/
/etc/containers/systemd/
/usr/share/containers/systemd/
For rootless container files:
$XDG_RUNTIME_DIR/containers/systemd/
$XDG_CONFIG_HOME/containers/systemd/ or ~/.config/containers/systemd/
/etc/containers/systemd/users/$(UID)
/etc/containers/systemd/users/
Once you place the [email protected]
in one of the directories - you can then run systemctl {--user} daemon-reload
followed by systemctl {--user} start [email protected]
to start the container.
Moving to Quadlets
I was keen to use Quadlets and quickly upgraded my hosts to Debian 13 to leverage the capability. As one can imagine, the biggest obstacle is converting compose files to the Quadlet unit service file format.
To make this easier, there’s an open-source tool available called podlet, created by the containers community, which outputs Quadlet files from an existing Podman command or compose file. I had about 20 services to convert and podlet
made it a very quick process.
My existing Home Assistant container had some complex device and group mappings to avoid running in privileged mode, but I was able to migrate without adjusting any of the output from podlet
.
My old compose file looked like:
version: "3.8"
services:
homeassistant:
container_name: homeassistant
image: ghcr.io/home-assistant/home-assistant:latest
runtime: crun
group_add:
- keep-groups
environment:
- TZ=Australia/Melbourne
- PYTHONPATH=/config/deps
volumes:
- /etc/podman/homeassistant/homeassistantconfig:/config
network_mode: host
devices:
- /dev/ttyUSB0:/dev/ttyUSB0 # zigbee USB
logging:
driver: journald
To migrate I ran:
podlet compose /etc/podman/homeassistant/compose.yaml > /etc/containers/systemd/users/1001/homeassistant.container
I wanted the container to auto-start on boot and recover automatically, so I added to the bottom of the service file the following snippet:
[Service]
Restart=always
[Install]
WantedBy=multi-user.target default.target
This resulted in the following Quadlet container file, which lives in /etc/containers/systemd/users/1001/homeassistant.container
:
# homeassistant.container
[Container]
AddDevice=/dev/ttyUSB0:/dev/ttyUSB0
ContainerName=homeassistant
Environment=TZ=Australia/Melbourne PYTHONPATH=/config/deps
Image=ghcr.io/home-assistant/home-assistant:latest
LogDriver=journald
Network=host
PodmanArgs=--group-add keep-groups
Volume=/etc/podman/homeassistant/homeassistantconfig:/config
GlobalArgs=--runtime crun
AutoUpdate=registry
[Service]
Restart=always
[Install]
WantedBy=multi-user.target default.target
Now the container is able to be managed by systemd as my rootless user, homeassistant
which has UID 1001
:
systemctl --user --daemon-reload
systemctl --user start homeassistant.service
systemctl --user status homeassistant.service
Handling upgrades
I also wanted to be able to check and apply upgrades easily, so for all my containers I add AutoUpdate=Registry
under the [Container]
section. This is to use the podman auto-update feature.
Don’t let the name fool you - it only auto-upgrades if you enable the podman-auto-update.timer
. I wanted to avoid auto-upgrades as many services (such as Home Assistant) have breaking changes on upgrade. I like my lamps working in my home unless I choose to break them :)
You can check for upgrades with a simple command:
podman auto-update --dry-run --format '{{.Image}} {{.Updated}}'
And apply them when needed:
podman auto-update
How things are a month later
I’ve been using Quadlets for well over a month and my systemd units are still running:
homeassistant@medusa:~$ systemctl status --user homeassistant.service
● homeassistant.service
Loaded: loaded (/etc/containers/systemd/users/1001/homeassistant.container; generated)
Active: active (running) since Tue 2025-08-26 00:31:53 AEST; 1 month 16 days ago
I don’t regret the move and am so glad this technology is available. It makes self-hosting so much easier. Hope this helps others who have a similar need.
See also
- https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html
- https://www.redhat.com/en/blog/quadlet-podman
Thank you for reading
Thank you so much for reading my post. If you have any feedback or queries, please reach out to me. My details are on the home page.