diff --git a/topics/podman-compose.md b/topics/podman-compose.md new file mode 100644 index 0000000..26f7704 --- /dev/null +++ b/topics/podman-compose.md @@ -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/` inside the container + +## See Also + +- `podman` — core container commands +- `podman-systemd` — Quadlet files for production deployment +- [Compose spec](https://compose-spec.io/) diff --git a/topics/podman-systemd.md b/topics/podman-systemd.md new file mode 100644 index 0000000..7261c1a --- /dev/null +++ b/topics/podman-systemd.md @@ -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)