Files
howtos/topics/podman-systemd.md
user 5aaa290b76 docs: add podman-compose and podman-systemd howtos
- podman-compose.md — full compose.yml reference, services, healthchecks,
  dependencies, networking, volumes, override files
- podman-systemd.md — Quadlet files (.container, .volume, .network, .kube,
  .build), multi-container stacks, lingering, auto-update, secrets
2026-02-21 21:18:26 +01:00

10 KiB

Podman Systemd & Quadlet

Run containers as systemd services — Quadlet is the modern, declarative approach.

Overview

Method Status Use case
podman generate systemd Deprecated Legacy, existing setups
Quadlet .container files Current New deployments

Quadlet

Quadlet converts declarative unit files into systemd services. Place files in the appropriate directory and daemon-reload — systemd does the rest.

File Locations

User Path
Rootless ~/.config/containers/systemd/
Root /etc/containers/systemd/

Unit Types

Extension Purpose
.container Container definition
.volume Named volume
.network Container network
.kube Kubernetes YAML deployment
.image Image pull/build
.build Image build instructions

.container — Single Service

# ~/.config/containers/systemd/myapp.container
[Unit]
Description=My Application
After=network-online.target

[Container]
Image=docker.io/library/myapp:latest
ContainerName=myapp
PublishPort=8080:80
Volume=myapp-data.volume:/app/data
Volume=%h/git/myapp/config:/app/config:ro,Z
Network=myapp.network
Environment=APP_ENV=production
Environment=TZ=Europe/Brussels
EnvironmentFile=%h/git/myapp/.env
Secret=db_pass
User=1000
Exec=--port 8080

# Health check
HealthCmd=curl -f http://localhost:80/health
HealthInterval=30s
HealthTimeout=5s
HealthRetries=3
HealthStartPeriod=10s

# Resource limits
PodmanArgs=--memory 512m --cpus 1.5

[Service]
Restart=on-failure
RestartSec=5
TimeoutStartSec=60
TimeoutStopSec=30

[Install]
WantedBy=default.target

Common Container Directives

Directive Purpose
Image Container image reference
ContainerName Explicit container name
PublishPort Port mapping (host:container)
Volume Volume mount (named or bind)
Network Network to join (reference .network file)
Environment Key=value env var (repeatable)
EnvironmentFile Load env from file
Secret Mount secret from podman secret store
User Container user (UID or name)
Exec Arguments appended to entrypoint
Entrypoint Override image entrypoint
PodmanArgs Extra podman run flags
Label Container label (repeatable)
Tmpfs Tmpfs mount
ReadOnly=true Read-only root filesystem
AutoUpdate=registry Enable podman auto-update
Notify=healthy Mark service ready when healthcheck passes
HealthCmd Health check command
HealthInterval Time between health checks
HealthTimeout Max time per check
HealthRetries Failures before unhealthy
HealthStartPeriod Grace period after start

Specifiers

Specifier Value
%h User home directory
%n Unit name
%N Unit name without suffix
%t Runtime directory ($XDG_RUNTIME_DIR)
%S State directory

.volume

# ~/.config/containers/systemd/myapp-data.volume
[Volume]
VolumeName=myapp-data
Label=app=myapp

Reference in .container as Volume=myapp-data.volume:/container/path.

.network

# ~/.config/containers/systemd/myapp.network
[Network]
NetworkName=myapp-net
Subnet=10.89.1.0/24
Gateway=10.89.1.1
Label=app=myapp

Reference in .container as Network=myapp.network.

.image

# ~/.config/containers/systemd/myapp.image
[Image]
Image=docker.io/library/myapp:latest

.build

# ~/.config/containers/systemd/myapp.build
[Build]
ImageTag=myapp:latest
SetWorkingDirectory=unit
File=Containerfile

Reference in .container as Image=myapp.build.

.kube — Kubernetes YAML

# ~/.config/containers/systemd/mystack.kube
[Unit]
Description=My application stack

[Kube]
Yaml=%h/git/myapp/pod.yml
PublishPort=8080:80
Network=myapp.network

[Service]
Restart=on-failure

[Install]
WantedBy=default.target

Multi-Container Stack

# ~/.config/containers/systemd/app.network
[Network]
NetworkName=app-net
# ~/.config/containers/systemd/app-db.volume
[Volume]
VolumeName=app-db-data
# ~/.config/containers/systemd/app-db.container
[Unit]
Description=PostgreSQL for app

[Container]
Image=docker.io/library/postgres:16-alpine
ContainerName=app-db
Volume=app-db.volume:/var/lib/postgresql/data
Network=app.network
Environment=POSTGRES_USER=appuser
Environment=POSTGRES_DB=myapp
EnvironmentFile=%h/.secrets/db.env
HealthCmd=pg_isready -U appuser -d myapp
HealthInterval=10s
HealthRetries=5

[Service]
Restart=on-failure

[Install]
WantedBy=default.target
# ~/.config/containers/systemd/app-web.container
[Unit]
Description=Application web server
After=app-db.service
Requires=app-db.service

[Container]
Image=docker.io/library/myapp:latest
ContainerName=app-web
PublishPort=8080:8000
Volume=%h/git/myapp/static:/app/static:ro,Z
Network=app.network
Environment=DB_HOST=app-db
Environment=DB_NAME=myapp
EnvironmentFile=%h/.secrets/db.env
Notify=healthy
HealthCmd=curl -f http://localhost:8000/health
HealthInterval=30s
HealthStartPeriod=15s

[Service]
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

Workflow

# After creating / editing Quadlet files:
systemctl --user daemon-reload

# Start
systemctl --user start app-web.service
systemctl --user start app-db.service

# Enable on boot (requires lingering)
loginctl enable-linger $USER
systemctl --user enable app-web.service
systemctl --user enable app-db.service

# Status / logs
systemctl --user status app-web.service
journalctl --user -u app-web.service -f

# Stop
systemctl --user stop app-web.service

# Validate quadlet files (dry run)
/usr/lib/systemd/system-generators/podman-system-generator --user --dryrun

Rootless & Lingering

Rootless (non-root) user services are tied to the user's login session by default. Without lingering, systemd kills all user services when the user logs out — containers included.

# Enable lingering (services survive logout)
loginctl enable-linger $USER

# Check status
loginctl show-user $USER --property=Linger
ls /var/lib/systemd/linger/            # file per lingered user

# Disable
loginctl disable-linger $USER
Linger state Behavior
Disabled (default) User services start on login, stop on logout
Enabled User services start at boot, persist after logout

Without lingering:

  • Containers stop when SSH session ends
  • Timers don't fire when logged out
  • WantedBy=default.target only activates at login

With lingering:

  • systemctl --user enable services start at boot
  • Containers run permanently, like root services
  • Required for any production rootless container

Rootless vs Root Quadlet

Rootless Root
Quadlet path ~/.config/containers/systemd/ /etc/containers/systemd/
systemctl systemctl --user systemctl
journalctl journalctl --user -u journalctl -u
Ports < 1024 Not allowed (unless sysctl) Allowed
Lingering Required Not applicable
Security No root needed Runs as root

Auto-Update

# In .container
[Container]
AutoUpdate=registry
Label=io.containers.autoupdate=registry
# Check for updates
podman auto-update --dry-run

# Apply updates
podman auto-update

# Systemd timer (auto-runs daily)
systemctl --user enable --now podman-auto-update.timer

Legacy: generate systemd

# From existing container (deprecated)
podman generate systemd --new --name myapp --files
mv container-myapp.service ~/.config/systemd/user/

systemctl --user daemon-reload
systemctl --user enable --now container-myapp.service

--new creates a new container each start (preferred over restarting existing).

Secrets

# Create secret
echo -n "s3cret" | podman secret create db_pass -
podman secret create app_key ./keyfile.txt

# List / inspect
podman secret ls
podman secret inspect db_pass

# Use in Quadlet
# [Container]
# Secret=db_pass
# Mounted at /run/secrets/db_pass inside container

# Remove
podman secret rm db_pass

Gotchas

  • daemon-reload is required after any Quadlet file change — systemd caches unit definitions
  • Quadlet service names are derived from the filename: myapp.container becomes myapp.service
  • After= and Requires= reference the .service name, not the .container filename
  • Rootless containers need loginctl enable-linger or they stop when the user logs out
  • %h in Quadlet expands to home directory — don't use $HOME or ~
  • Volume references must match the .volume filename: Volume=mydata.volume:/path
  • The Quadlet generator path varies by distro — check with rpm -ql podman | grep generator or dpkg -L podman
  • Quadlet .build requires the source directory — set SetWorkingDirectory=unit to use the Quadlet file's location
  • Notify=healthy requires a HealthCmd — without it, the service never reaches "ready"
  • podman auto-update only works with fully qualified image names (include registry)

See Also

  • podman — core container commands
  • podman-compose — compose file orchestration
  • systemd — service management fundamentals
  • Quadlet docs