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