Self-updating Containers on Linux with Quadlet aka podman-system-generator

dholth

Daniel Holth

Posted on October 30, 2024

Self-updating Containers on Linux with Quadlet aka podman-system-generator

If you are using a modern Linux system like Fedora 40, you may want to configure systemd which manages services, and podman to run containers as an alternative to Docker. This setup makes it possible to run rootless containers as a normal user without worrying about giving the container global permissions.

systemd uses .service files to control what to run. For example, samba, a file server, has /usr/lib/systemd/system/samba.service

[Unit]
Description=Samba AD Daemon
Documentation=man:samba(8) man:samba(7) man:smb.conf(5)
Wants=network-online.target
After=network.target network-online.target

[Service]
Type=notify
PIDFile=/run/samba.pid
LimitNOFILE=16384
EnvironmentFile=-/etc/sysconfig/samba
ExecStart=/usr/sbin/samba --foreground --no-process-group $SAMBAOPTIONS
ExecReload=/bin/kill -HUP $MAINPID
Environment=KRB5CCNAME=FILE:/run/samba/krb5cc_samba

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

We want our container to start automatically on boot and restart when needed, so we need a systemd .service file. In the old days we would create a container using podman and then manually put a .service file for that container somewhere.

This works for a while but our service references an exact container. When we want to update the container, we have to re-write the service file to reference the update. On my system the service file referenced the new container in one place and the old one in the other, I forgot to update both, and systemd kept restarting my container.

Six months later, I discovered Quadlet.

Quadlet is a generator to alleviate the burden of manually writing .service files, one of many found in /usr/lib/systemd/system-generators:

# ls -1 /usr/lib/systemd/system-generators
kdump-dep-generator.sh
nfs-server-generator
podman-system-generator
rpc-pipefs-generator
selinux-autorelabel-generator.sh
systemd-bless-boot-generator
systemd-cryptsetup-generator
systemd-debug-generator
systemd-fstab-generator
systemd-getty-generator
systemd-gpt-auto-generator
systemd-hibernate-resume-generator
systemd-integritysetup-generator
systemd-rc-local-generator
systemd-run-generator
systemd-system-update-generator
systemd-sysv-generator
systemd-veritysetup-generator
zram-generator
Enter fullscreen mode Exit fullscreen mode

Generators convert higer-level configuration to systemd early on during startup, when we call systemctl daemon-reload, or when we call systemctl --user daemon-reload for rootless services.

Examples

Mosquitto is a simple message broker I've chosen to run as root. After placing the .container file in the right place and running systemctl daemon-reload (or rebooting), we can run systemctl start mosquitto. All the podman commands to pull, run docker.io/library/eclipse-mosquitto:latest are taken care of for us.

# cat /etc/containers/systemd/mosquitto.container
[Container]
ContainerName=mosquitto
HostName=hass
Image=docker.io/library/eclipse-mosquitto:latest
Volume=/etc/mosquitto:/mosquitto/config
Volume=/mosquitto/data
Volume=/mosquitto/log
# mqtt
PublishPort=1883:1883
Enter fullscreen mode Exit fullscreen mode

The service winds up in /etc/containers/systemd/mosquitto.container:

# Automatically generated by /usr/lib/systemd/system-generators/podman-system-generator
#
# unifi.container
[X-Container]
ContainerName=mosquitto
HostName=hass
Image=docker.io/library/eclipse-mosquitto:latest
Volume=/etc/mosquitto:/mosquitto/config
Volume=/mosquitto/data
Volume=/mosquitto/log
# mqtt
PublishPort=1883:1883

[Unit]
Wants=network-online.target
After=network-online.target
SourcePath=/etc/containers/systemd/mosquitto.container
RequiresMountsFor=%t/containers
RequiresMountsFor=/etc/mosquitto

[Service]
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=mosquitto --cidfile=%t/%N.cid --replace --rm --cgroups=split --sdnotify=conmon -d -v /etc/mosquitto:/mosquitto/config -v /mosquitto/data -v /mosquitto/log --publish 1883:1883 --hostname hass docker.io/library/eclipse-mosquitto:latest
Enter fullscreen mode Exit fullscreen mode

Rootless Containers

The rootless .container files go into ~/.config/containers/systemd. I'm using one to run Home Assistant.

$ find .* -name \*.container
.config/containers/systemd/homeassistant.container
Enter fullscreen mode Exit fullscreen mode

My container file looks like this. I also gave it permission to /dev/ttyACM0 for a ZigBee radio, and CAP_NET_RAW,CAP_NET_BIND_SERVICE to attempt to allow it to bind to any port, and monitor the network for new devices.

$ cat homeassistant.container
[Unit]
Description=Homeassistant

[Container]
AddCapability=CAP_NET_RAW,CAP_NET_BIND_SERVICE
AddDevice=/dev/ttyACM0
ContainerName=homeassistant
Environment=TZ=America/New_York
Image=ghcr.io/home-assistant/home-assistant:latest
Network=host
Volume=/home/me/hass/config:/config
PodmanArgs=--privileged --group-add=keep-groups
# GroupAdd=keep-groups # future release? https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html

[Install]
WantedBy=default.target
Enter fullscreen mode Exit fullscreen mode

The generated service file winds up in /run/<uid>/systemd. Now an up-to-date Home Assistant container runs as my user when I start my Fedora server.

/run/user/1000/systemd$ cat generator/*
# Automatically generated by /usr/lib/systemd/user-generators/podman-user-generator
#
[Unit]
Wants=network-online.target
After=network-online.target
Description=Homeassistant
SourcePath=/home/me/.config/containers/systemd/homeassistant.container
RequiresMountsFor=%t/containers
RequiresMountsFor=/home/me/hass/config

[X-Container]
AddCapability=CAP_NET_RAW,CAP_NET_BIND_SERVICE
AddDevice=/dev/ttyACM0
ContainerName=homeassistant
Environment=TZ=America/New_York
Image=ghcr.io/home-assistant/home-assistant:latest
Network=host
Volume=/home/me/hass/config:/config
PodmanArgs=--privileged --group-add=keep-groups

# GroupAdd=keep-groups # future release? https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html

[Install]
WantedBy=default.target

[Service]
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=homeassistant --cidfile=%t/%N.cid --replace --rm --cgroups=split --network=host --sdnotify=conmon -d --device=/dev/ttyACM0 --cap-add=cap_net_raw,cap_net_bind_service -v /home/me/hass/config:/config --env TZ=America/New_York --privileged --group-add=keep-groups ghcr.io/home-assistant/home-assistant:latest
Enter fullscreen mode Exit fullscreen mode

Conclusion

systemd has many features including user-mode process management, powerful cron-like service timers, and now container management features that long-term Linux users may not be familiar with. After struggling through the sometimes-spotty documentation of this newish feature, we've managed to create a maintainable set of containerized services on our home Linux server.

💖 💪 🙅 🚩
dholth
Daniel Holth

Posted on October 30, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related