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