- 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
10 KiB
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.targetonly activates at login
With lingering:
systemctl --user enableservices 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-reloadis required after any Quadlet file change — systemd caches unit definitions- Quadlet service names are derived from the filename:
myapp.containerbecomesmyapp.service After=andRequires=reference the.servicename, not the.containerfilename- Rootless containers need
loginctl enable-lingeror they stop when the user logs out %hin Quadlet expands to home directory — don't use$HOMEor~- Volume references must match the
.volumefilename:Volume=mydata.volume:/path - The Quadlet generator path varies by distro — check with
rpm -ql podman | grep generatorordpkg -L podman - Quadlet
.buildrequires the source directory — setSetWorkingDirectory=unitto use the Quadlet file's location Notify=healthyrequires aHealthCmd— without it, the service never reaches "ready"podman auto-updateonly works with fully qualified image names (include registry)
See Also
podman— core container commandspodman-compose— compose file orchestrationsystemd— service management fundamentals- Quadlet docs