Files
howtos/topics/podman-compose.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

8.7 KiB

Podman Compose

Multi-container orchestration with Compose files — podman's docker-compose equivalent.

Setup

# 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

# 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

# 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

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

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):

# .env
APP_VERSION=2.1
POSTGRES_PASSWORD=secret
EXTERNAL_PORT=8080
# Variable substitution in compose.yml
services:
  app:
    image: myapp:${APP_VERSION}
    ports:
      - "${EXTERNAL_PORT:-8080}:80"    # with default

Healthchecks

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

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

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

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

# 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
# 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

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