- 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
8.7 KiB
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-composeanddocker-composeare mostly compatible but edge cases differ — test both if porting:Zon volumes is required for SELinux (Fedora/RHEL) — omitting it causes silent permission errorsdepends_on: service_healthyrequires ahealthcheckon the dependency — no healthcheck = hangsenv_filevalues are literal — no shell expansion, no quotes needed (quotes become part of value).envis loaded for compose variable substitution only — useenv_fileto pass vars into containers- Named volumes persist across
down— onlydown -vremoves them podman-composeruns 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_nameprevents 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 commandspodman-systemd— Quadlet files for production deployment- Compose spec