Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Podman, Quadlets, and systemd

For a rootless setup, we can use quadlets and systemd to manage the container lifecycle.

Important

If this is the first container managed with quadlets for your user, ensure that linger is enabled so your containers are not killed after logging out.

sudo loginctl enable-linger <username>

Step One

Copy quadlet files to ~/.config/containers/systemd/tuwunel

tuwunel.container

tuwunel container quadlet
# tuwenel.container

[Unit]
Description=Tuwunel Matrix Homeserver

[Container]
ContainerName=tuwunel-homeserver
Image=ghcr.io/matrix-construct/tuwunel:latest
PublishPort=8008:8008
Volume=tuwunel-db:/var/lib/tuwunel/

#Example location in ~/tuwunel/config/
Volume=%h/tuwunel/config/tuwunel.toml:/etc/tuwunel.toml
EnvironmentFile=tuwunel.env

# The first boot after an upgrade can run a database migration that must not be
# interrupted partway, so both stop deadlines have to clear the longest one.
# This is podman's, raising the ten second default it applies to the container
# on the way down. StopTimeout= is the native key for it, but quadlet rejects
# the whole file on an unknown key and only learned that key in podman 5.0, so
# spelling it as a passthrough argument keeps the unit generating on the 4.x
# that Ubuntu 24.04 LTS and RHEL 9 ship.
PodmanArgs=--stop-timeout=1800

# The published image is OCI format, and the OCI image spec carries no
# healthcheck field, so the image's own HEALTHCHECK is invisible to Podman.
# Declaring it here restores the same probe Docker users get. The exec form is
# required: a bare string runs through a shell, and the image has none.
HealthCmd=["/usr/bin/tuwunel", "--health-check"]
HealthInterval=30s
HealthTimeout=15s

# The probe answers whether the server is serving, and a migrating one is not,
# so the start period is what keeps a long first boot reported as starting
# rather than unhealthy. An unhealthy container invites the restart that kills
# a migration mid-write.
HealthStartPeriod=1800s
HealthRetries=3

[Service]
# The other stop deadline. Quadlet writes no TimeoutStopSec= of its own, so
# without this systemd's DefaultTimeoutStopSec (90s) SIGKILLs the container
# cgroup mid-migration however long podman was told to wait.
TimeoutStopSec=1830

# Uncomment when your system is properly configured, restart=always can mask start up errors. 
#Restart=always

[Install]
WantedBy=default.target

tuwunel-db.volume

tuwunel database volume quadlet
[Volume]
VolumeName=tuwunel-db

tuwunel.env

tuwunel environment variable quadlet
TUWUNEL_SERVER_NAME="your.server.tld"
TUWUNEL_PORT=8008
TUWUNEL_MAX_REQUEST_SIZE=20000000
TUWUNEL_ALLOW_REGISTRATION=true
TUWUNEL_REGISTRATION_TOKEN=<replace with a passphrase or random string>
TUWUNEL_ALLOW_FEDERATION=true
TUWUNEL_TRUSTED_SERVERS=["matrix.org"]
TUWUNEL_LOG=info

#Listen on this host for IPv4 and v6
TUWUNEL_ADDRESS=["0.0.0.0", "::"]

#Tell Tuwunel to use the user config file 
TUWUNEL_CONFIG=/etc/tuwunel.toml

mkdir -p ~/.config/containers/systemd/tuwunel

Step Two

Modify tuwunel.env and tuwunel.toml to desired values. This can be saved in your user home directory if desired.

Step Three

  • Reload daemon to generate our systemd unit files:
systemctl --user daemon-reload

Step Four

  • Start tuwunel:
systemctl --user start tuwunel

Logging

To check the logs, run:

systemctl --user status tuwunel

or

podman logs tuwunel-homeserver

Health checking outside a quadlet

The quadlet above declares the health check explicitly, so quadlet users get the same probe Docker users get. A bare podman run of the published image does not, and the reason is worth knowing rather than working around blindly.

The image is published in OCI format, and the health check rides in its config as an extra field, which is where Docker reads it from. Podman does not read that field out of an OCI config (containers/podman#25454, #18904, both open), so it reports no health check at all:

$ podman inspect --format '{{json .HealthCheck}}' ghcr.io/matrix-construct/tuwunel:latest
null
$ docker inspect --format '{{json .Config.Healthcheck}}' ghcr.io/matrix-construct/tuwunel:latest
{"Test":["CMD","tuwunel","--health-check"],...}

Nothing is missing from the image and nothing needs rebuilding in another format. Pass the probe on the command line instead:

podman run -d --name tuwunel \
  --health-cmd '["/usr/bin/tuwunel", "--health-check"]' \
  --health-interval 30s \
  --health-timeout 15s \
  --health-start-period 1800s \
  --health-retries 3 \
  ghcr.io/matrix-construct/tuwunel:latest

The JSON array form is required. A bare string runs through a shell, and the image is built FROM scratch with none. See Health during a migration for why the start period is that wide.

Troubleshooting systemd unit file generation

Look for errors in the output: /usr/lib/systemd/system-generators/podman-system-generator --user --dryrun