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:
372
topics/podman-compose.md
Normal file
372
topics/podman-compose.md
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
# Podman Compose
|
||||||
|
|
||||||
|
> Multi-container orchestration with Compose files — podman's docker-compose equivalent.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install
|
||||||
|
sudo apt install podman-compose # Debian/Ubuntu
|
||||||
|
sudo dnf install podman-compose # Fedora/RHEL
|
||||||
|
pip install podman-compose # pip fallback
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
podman-compose version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compose File Reference
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# compose.yml (or docker-compose.yml — both recognized)
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: web
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- ./html:/usr/share/nginx/html:ro,Z
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro,Z
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
depends_on:
|
||||||
|
app:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Brussels
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Containerfile
|
||||||
|
args:
|
||||||
|
APP_VERSION: "2.1"
|
||||||
|
container_name: app
|
||||||
|
expose:
|
||||||
|
- "8000" # internal only (not published)
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./src:/app/src:Z
|
||||||
|
- app-data:/app/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
APP_ENV: production
|
||||||
|
DB_HOST: db
|
||||||
|
DB_NAME: myapp
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
restart: on-failure
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
cpus: "1.0"
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: db
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro,Z
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: appuser
|
||||||
|
POSTGRES_PASSWORD_FILE: /run/secrets/db_pass
|
||||||
|
POSTGRES_DB: myapp
|
||||||
|
secrets:
|
||||||
|
- db_pass
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5432:5432" # localhost only
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: redis
|
||||||
|
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
|
||||||
|
volumes:
|
||||||
|
- redis-data:/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
redis-data:
|
||||||
|
app-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend:
|
||||||
|
backend:
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
db_pass:
|
||||||
|
file: ./secrets/db_password.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start / stop
|
||||||
|
podman-compose up -d # detached
|
||||||
|
podman-compose up -d --build # rebuild before start
|
||||||
|
podman-compose down # stop + remove containers
|
||||||
|
podman-compose down -v # also remove volumes
|
||||||
|
|
||||||
|
# Specific services
|
||||||
|
podman-compose up -d web app # start only these
|
||||||
|
podman-compose stop db # stop one service
|
||||||
|
podman-compose restart app
|
||||||
|
|
||||||
|
# Build
|
||||||
|
podman-compose build # all services with build:
|
||||||
|
podman-compose build app # specific service
|
||||||
|
podman-compose build --no-cache app # force rebuild
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
podman-compose logs # all services
|
||||||
|
podman-compose logs -f app # follow one service
|
||||||
|
podman-compose logs --tail 100 app db # last 100 lines, multiple
|
||||||
|
|
||||||
|
# Status
|
||||||
|
podman-compose ps
|
||||||
|
podman-compose top # processes in each container
|
||||||
|
|
||||||
|
# Execute
|
||||||
|
podman-compose exec app bash
|
||||||
|
podman-compose exec db psql -U appuser -d myapp
|
||||||
|
podman-compose exec -T app python manage.py migrate # no TTY (scripts)
|
||||||
|
|
||||||
|
# Run one-off command (new container)
|
||||||
|
podman-compose run --rm app python manage.py shell
|
||||||
|
|
||||||
|
# Pull latest images
|
||||||
|
podman-compose pull
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Configuration
|
||||||
|
|
||||||
|
### Build Options
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: ./app # build context directory
|
||||||
|
dockerfile: Containerfile # relative to context
|
||||||
|
args:
|
||||||
|
APP_VERSION: "2.0"
|
||||||
|
BUILD_ENV: production
|
||||||
|
target: production # multi-stage target
|
||||||
|
cache_from:
|
||||||
|
- myapp:latest
|
||||||
|
image: myapp:latest # tag the built image
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
# Inline key-value
|
||||||
|
environment:
|
||||||
|
APP_ENV: production
|
||||||
|
DEBUG: "false"
|
||||||
|
|
||||||
|
# From file
|
||||||
|
env_file:
|
||||||
|
- .env # default
|
||||||
|
- .env.production # override
|
||||||
|
|
||||||
|
# Pass host variable through (no value = inherit)
|
||||||
|
environment:
|
||||||
|
- HOME
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env` file (auto-loaded for variable substitution in compose.yml):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
APP_VERSION=2.1
|
||||||
|
POSTGRES_PASSWORD=secret
|
||||||
|
EXTERNAL_PORT=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Variable substitution in compose.yml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: myapp:${APP_VERSION}
|
||||||
|
ports:
|
||||||
|
- "${EXTERNAL_PORT:-8080}:80" # with default
|
||||||
|
```
|
||||||
|
|
||||||
|
### Healthchecks
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||||
|
interval: 30s # time between checks
|
||||||
|
timeout: 10s # max time per check
|
||||||
|
retries: 3 # failures before unhealthy
|
||||||
|
start_period: 30s # grace period after start
|
||||||
|
start_interval: 5s # interval during start_period
|
||||||
|
```
|
||||||
|
|
||||||
|
| Test form | Example |
|
||||||
|
|-----------|---------|
|
||||||
|
| `CMD` | `["CMD", "curl", "-f", "http://localhost/health"]` |
|
||||||
|
| `CMD-SHELL` | `["CMD-SHELL", "pg_isready \|\| exit 1"]` |
|
||||||
|
| `NONE` | Disable inherited healthcheck |
|
||||||
|
|
||||||
|
### Dependency Conditions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_started # default — just started
|
||||||
|
db:
|
||||||
|
condition: service_healthy # wait for healthcheck to pass
|
||||||
|
migrations:
|
||||||
|
condition: service_completed_successfully # wait for exit 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Networking
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
networks:
|
||||||
|
frontend:
|
||||||
|
aliases:
|
||||||
|
- api # extra DNS name on this network
|
||||||
|
backend:
|
||||||
|
ipv4_address: 10.89.0.10 # static IP
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend:
|
||||||
|
driver: bridge
|
||||||
|
backend:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 10.89.0.0/24
|
||||||
|
gateway: 10.89.0.1
|
||||||
|
external-net:
|
||||||
|
external: true # pre-existing network
|
||||||
|
```
|
||||||
|
|
||||||
|
Containers on the same network resolve each other by service name.
|
||||||
|
|
||||||
|
### Volumes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
volumes:
|
||||||
|
# Bind mounts
|
||||||
|
- ./src:/app/src:Z # relative path
|
||||||
|
- /host/path:/container/path:ro # absolute, read-only
|
||||||
|
|
||||||
|
# Named volumes
|
||||||
|
- app-data:/app/data
|
||||||
|
|
||||||
|
# Tmpfs
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmp
|
||||||
|
tmpfs:
|
||||||
|
size: 100000000 # 100MB
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
app-data: # managed by podman
|
||||||
|
shared-data:
|
||||||
|
external: true # pre-existing volume
|
||||||
|
backup:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: none
|
||||||
|
o: bind
|
||||||
|
device: /mnt/backup/app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Patterns
|
||||||
|
|
||||||
|
### Override File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# compose.override.yml — auto-merged with compose.yml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
volumes:
|
||||||
|
- ./src:/app/src:Z # live reload
|
||||||
|
environment:
|
||||||
|
APP_ENV: development
|
||||||
|
DEBUG: "true"
|
||||||
|
ports:
|
||||||
|
- "5678:5678" # debugger port
|
||||||
|
command: python -m debugpy --listen 0.0.0.0:5678 -m src.app
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Production (skip override)
|
||||||
|
podman-compose -f compose.yml up -d
|
||||||
|
|
||||||
|
# Explicit files
|
||||||
|
podman-compose -f compose.yml -f compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wait-for Pattern
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
# OR with a wrapper script:
|
||||||
|
command: ["./wait-for-it.sh", "db:5432", "--", "python", "app.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- `podman-compose` and `docker-compose` are mostly compatible but edge cases differ — test both if porting
|
||||||
|
- `:Z` on volumes is required for SELinux (Fedora/RHEL) — omitting it causes silent permission errors
|
||||||
|
- `depends_on: service_healthy` requires a `healthcheck` on the dependency — no healthcheck = hangs
|
||||||
|
- `env_file` values are literal — no shell expansion, no quotes needed (quotes become part of value)
|
||||||
|
- `.env` is loaded for compose variable substitution only — use `env_file` to pass vars into containers
|
||||||
|
- Named volumes persist across `down` — only `down -v` removes them
|
||||||
|
- `podman-compose` runs containers independently, not in a pod by default — use `--podman-run-args="--pod"` or configure per-project
|
||||||
|
- DNS resolution between containers only works on user-created networks
|
||||||
|
- `container_name` prevents scaling (`podman-compose up --scale app=3`)
|
||||||
|
- Secret files are mounted read-only at `/run/secrets/<name>` inside the container
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- `podman` — core container commands
|
||||||
|
- `podman-systemd` — Quadlet files for production deployment
|
||||||
|
- [Compose spec](https://compose-spec.io/)
|
||||||
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