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

383 lines
10 KiB
Markdown

# 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
```ini
# ~/.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
```ini
# ~/.config/containers/systemd/myapp-data.volume
[Volume]
VolumeName=myapp-data
Label=app=myapp
```
Reference in `.container` as `Volume=myapp-data.volume:/container/path`.
## .network
```ini
# ~/.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
```ini
# ~/.config/containers/systemd/myapp.image
[Image]
Image=docker.io/library/myapp:latest
```
## .build
```ini
# ~/.config/containers/systemd/myapp.build
[Build]
ImageTag=myapp:latest
SetWorkingDirectory=unit
File=Containerfile
```
Reference in `.container` as `Image=myapp.build`.
## .kube — Kubernetes YAML
```ini
# ~/.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
```ini
# ~/.config/containers/systemd/app.network
[Network]
NetworkName=app-net
```
```ini
# ~/.config/containers/systemd/app-db.volume
[Volume]
VolumeName=app-db-data
```
```ini
# ~/.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
```
```ini
# ~/.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
```bash
# 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.
```bash
# 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
```ini
# In .container
[Container]
AutoUpdate=registry
Label=io.containers.autoupdate=registry
```
```bash
# 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
```bash
# 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
```bash
# 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](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html)