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
This commit is contained in:
382
topics/podman-systemd.md
Normal file
382
topics/podman-systemd.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user