Chapter 68: Podman in Depth: Install, Rootless Containers, Pods, Compose, Kube YAML and Alternatives
68.4 podman compose and podman-compose: running Docker Compose files with Podman
Why: the problem
Most teams already have a docker-compose.yml. The good news is that Podman can run it, and there are two ways to do so. The first is podman-compose, a Python program from the containers project that reads the compose file and turns each service into podman commands. The second is podman compose, a built-in Podman subcommand that does not implement compose itself: it finds a compose provider, either docker-compose or podman-compose, and runs it against Podman's Docker-compatible API socket. Both read the same file format, so your YAML does not change. Why two tools? Docker Compose v2 is the reference implementation of the Compose Specification and supports every feature, including profiles, depends_on with healthchecks, build secrets and watch mode. It talks to a Docker API socket, and Podman can provide one with podman system service or the podman.socket systemd unit. podman-compose is lighter, needs only Python, and can put all services of a project into one pod with --in-pod, which is closer to Kubernetes. In practice teams at companies like Red Hat, Thoughtworks and Indian banks on RHEL use docker-compose through the Podman socket when they need full compatibility, and podman-compose when they want a pure Podman tool with no Docker binary on the host. What changes compared to Docker? Images should use full names such as docker.io/library/postgres:16-alpine, because Podman may ask which registry to use for short names. Rootless ports below 1024 need the sysctl from 68.2. Volume ownership needs :U or :Z when the image runs as a non-root user. Everything else, including networks, named volumes, environment files, healthchecks and depends_on, works the same way. In this lesson we install both tools, run the orders compose file with podman-compose, run the same file with podman compose and docker-compose over the Podman socket, use healthchecks and depends_on, scale a service, and finish by generating systemd-friendly units, which leads into Quadlet in 68.5.
What: every term in plain English
- Compose Specification — Open YAML format for multi-container apps, used by Docker and Podman.
- podman-compose — Python tool that converts compose services into podman commands.
- podman compose — Built-in wrapper that runs a compose provider against Podman.
- Compose provider — The external binary podman compose calls: docker-compose or podman-compose.
- podman.socket — systemd unit that exposes a Docker-compatible API for the user.
- DOCKER_HOST — Environment variable pointing docker-compose to the Podman socket.
- --in-pod — podman-compose option that places all services in one pod.
- Short-name resolution — How Podman picks a registry for names like postgres:16.
- healthcheck — Test command that marks a container healthy or unhealthy.
- depends_on condition — Start order based on service_healthy or service_started.
How: step by step
- Install podman-compose. Version printed.
- Write the orders compose file with full image names. File ready.
- Run it with podman-compose. Three containers up.
- Check health and call the API. Healthy, 200.
- Enable the Podman socket. API socket listening.
- Run the same file with podman compose and docker-compose. Same result.
- Scale the API and look at logs. Two replicas.
- Tear everything down. Clean host.
Think of the compose file as a recipe card and the tools as cooks. Docker Compose is the head chef who knows every advanced technique and needs the kitchen's main service counter, the API socket, to work. podman-compose is a home cook who reads the same card and does most dishes well using only basic tools. podman compose is the waiter who takes your order and hands it to whichever cook is available. The recipe card stays the same, which is why teams can move from Docker to Podman without rewriting their YAML. When you migrate a real project, first change images to full names, then fix low ports and volume ownership, run the stack once with podman-compose, and keep docker-compose over the socket as the fallback for any feature that is missing.
Example 1: Run the orders compose file with podman-compose
Goal. Run the orders compose file with podman-compose.
compose.sh
set -euo pipefail
python3 -m pip install --user podman-compose==1.5.0
mkdir -p ~/podman-lab/orders && cd ~/podman-lab/orders
printf 'POSTGRES_USER=orders\nPOSTGRES_DB=orders\nPOSTGRES_PASSWORD=REPLACE_WITH_DB_PASSWORD\n' > db.env
printf '%s\n' \
'services:' \
' db:' \
' image: docker.io/library/postgres:16-alpine' \
' env_file: db.env' \
' volumes: ["pgdata:/var/lib/postgresql/data:U"]' \
' healthcheck: {test: ["CMD", "pg_isready", "-U", "orders"], interval: 5s, retries: 10}' \
' cache:' \
' image: docker.io/library/redis:7-alpine' \
' api:' \
' image: registry.example.com/shop/orders-api:1.4.0' \
' environment: {DB_HOST: db, DB_USER: orders, REDIS_HOST: cache}' \
' env_file: db.env' \
' ports: ["8080:8080"]' \
' depends_on: {db: {condition: service_healthy}, cache: {condition: service_started}}' \
'volumes: {pgdata: {}}' > compose.yaml
podman-compose version
podman-compose up -d
podman-compose ps
sleep 10 && curl -s localhost:8080/health
What each line does
pip install --user— Installs podman-compose for your user only, no sudo; Fedora also has dnf install podman-compose.printf into db.env— Writes the placeholder secret file without a heredoc; keep real values out of Git.Full image names— docker.io/library/... avoids the short-name prompt on Podman.:U on pgdata— Chowns the named volume to the Postgres user inside the rootless namespace.service_healthy— The API starts only after pg_isready succeeds.
Green output
$ bash compose.sh
podman-compose version 1.5.0
podman version 6.1.4
...
orders_db_1 docker.io/library/postgres:16-alpine Up 12 seconds (healthy)
orders_cache_1 docker.io/library/redis:7-alpine Up 11 seconds
orders_api_1 registry.example.com/shop/orders-api:1.4.0 Up 2 seconds 0.0.0.0:8080->8080/tcp
{"status":"ok","db":"up","redis":"up"}
Expected result. The unchanged compose format runs rootless on Podman with no daemon. The healthcheck held the API back until Postgres was ready, exactly as with Docker.
Example 2: Same file with podman compose and docker-compose over the Podman socket
Goal. Same file with podman compose and docker-compose over the Podman socket.
socket.sh
cd ~/podman-lab/orders
podman-compose down
systemctl --user enable --now podman.socket
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock http://d/_ping; echo
podman compose version
podman compose up -d
podman compose ps --format 'table {{.Name}}\t{{.Status}}'
podman compose logs --tail 2 api
What each line does
podman.socket— Starts the API only when something connects, so there is still no always-on daemon.DOCKER_HOST— Tells docker-compose where to send API calls; the socket lives in your runtime dir._ping— Docker API health endpoint; OK means compatible tools can connect.podman compose— Picks docker-compose if installed, otherwise podman-compose; it prints which provider it runs.
Green output
$ bash socket.sh
OK
>>>> Executing external compose provider "/usr/libexec/docker/cli-plugins/docker-compose". <<<<
Docker Compose version v2.40.3
Container orders-db-1 Healthy
Container orders-cache-1 Started
Container orders-api-1 Started
NAME STATUS
orders-api-1 Up 3 seconds
orders-cache-1 Up 4 seconds
orders-db-1 Up 9 seconds (healthy)
api-1 | listening on :8080
api-1 | db pool ready
Expected result. With the socket on, the full Docker Compose v2 runs against Podman. Teams that rely on newer compose features, such as profiles and watch, use this path and still get rootless, daemonless containers.
Example 3: Scale, run in one pod, and clean up
Goal. Scale, run in one pod, and clean up.
scale.sh
cd ~/podman-lab/orders
podman compose down
sed -i 's/"8080:8080"/"8080-8081:8080"/' compose.yaml
podman-compose up -d --scale api=2
podman ps --format '{{.Names}} {{.Ports}}' | grep api
podman-compose down
podman-compose --in-pod true up -d
podman pod ps --format '{{.Name}} {{.NumberOfContainers}}'
podman-compose down -v
systemctl --user disable --now podman.socket
What each line does
Port range— Two replicas cannot share 8080 on the host, so a range gives each its own port.--scale api=2— Starts two API containers from one service.--in-pod true— Puts all services in one pod named pod_orders, so they could also use localhost.down -v— Also removes the named volume; use only in labs.
Green output
$ bash scale.sh
orders_api_1 0.0.0.0:8080->8080/tcp
orders_api_2 0.0.0.0:8081->8080/tcp
pod_orders 4
Expected result. Scaling and pods both work from the same file. For real load balancing across replicas you would add a proxy such as Traefik or Caddy, or move to Kubernetes, which does it with a Service.
Every command, step by step (Podman lab)
-
Install
python3 -m pip install --user podman-composeWhat it does: Tool.
Successfully installed podman-compose-1.5.0 -
Up
podman-compose up -dWhat it does: Start.
orders_db_1 -
Ps
podman-compose psWhat it does: Status.
(healthy) -
Health
curl -s localhost:8080/healthWhat it does: API.
{"status":"ok"} -
Socket
systemctl --user enable --now podman.socketWhat it does: API.
Created symlink -
Ping
curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock http://d/_pingWhat it does: Check.
OK -
Wrapper
podman compose up -dWhat it does: Provider.
Executing external compose provider -
Logs
podman compose logs --tail 2 apiWhat it does: Logs.
listening on :8080 -
Scale
podman-compose up -d --scale api=2What it does: Two.
orders_api_2 -
Down
podman-compose down -vWhat it does: Clean.
orders_db_1
When it is red 1
Error: short-name "postgres:16-alpine" did not resolve to an alias and no unqualified-search registries are defined
Cause. Short image name.
Fix, step by step
- Use docker.io/library/postgres:16-alpine, or add docker.io to unqualified-search-registries in registries.conf.
When it is red 2
Cannot connect to the Docker daemon at unix:///var/run/docker.sock
Cause. docker-compose not pointed at Podman.
Fix, step by step
- Enable podman.socket and export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock.
When it is red 3
Error: looking up compose provider failed
Cause. Neither docker-compose nor podman-compose installed.
Fix, step by step
- Install one of them, or set compose_providers in containers.conf.
When it is red 4
initdb: could not change permissions of directory
Cause. Volume owned by the wrong mapped UID.
Fix, step by step
- Add :U to the volume or use podman unshare chown.
When it is red 5
bind: address already in use on 8080
Cause. Scaled service with a fixed host port.
Fix, step by step
- Use a port range or remove the host port and front with a proxy.
Practice tasks
Easy Task 1: Install
Why: Tool.
What to do: Install podman-compose.
- version.
Solution
podman-compose versionEasy Task 2: Up
Why: Start.
What to do: Start the orders file.
- ps.
Solution
podman-compose psEasy Task 3: Names
Why: Images.
What to do: Change images to full names.
- No prompt.
Solution
grep image compose.yamlMedium Task 4: Health
Why: Order.
What to do: Add a healthcheck and service_healthy.
- (healthy).
Solution
podman psMedium Task 5: Socket
Why: API.
What to do: Enable podman.socket and ping.
- OK.
Solution
curl --unix-socket ...Medium Task 6: Wrapper
Why: Provider.
What to do: Run podman compose up.
- Provider line.
Solution
podman compose psHard Task 7: Scale
Why: Replicas.
What to do: Scale API to two with a port range.
- Two ports.
Solution
podman psHard Task 8: Paytm demo
Why: Main task.
What to do: Full payments compose on Podman.
- All up.
Solution
podman compose psHomework: try at home
For a Paytm-style payments demo, take an existing docker-compose.yml with Postgres, Redis and a payments API, change images to full names, add a healthcheck and service_healthy, run it rootless with podman-compose, then again with podman compose over the socket, scale the API to two replicas on 9090-9091, and clean up.
Recap
- Podman runs Compose files with podman-compose or with podman compose plus a provider.
- podman.socket gives a Docker-compatible API so docker-compose works unchanged.
- Use full image names and :U volumes when running rootless.
- Healthchecks, depends_on, networks and volumes behave like Docker.
- --in-pod groups a project into one pod, closer to Kubernetes.
FAQ
Q. Which should I pick, podman-compose or docker-compose?
Use docker-compose over the Podman socket when you need every Compose feature or when the same file must behave exactly like it does on Docker. Use podman-compose when you want no Docker binary on the host, for example on locked-down RHEL servers, and your file uses common features only.
Q. Is the Podman socket a daemon?
It is socket-activated: systemd starts podman system service when a client connects and it exits after a timeout. It runs as your user, not root, so it does not reintroduce the root daemon problem.
Q. Does compose watch or build work?
Build works with both tools and uses Buildah under the hood. Watch mode needs docker-compose v2 over the socket.
Q. Can I use Docker Desktop compose files on a Mac with Podman?
Yes. podman machine exposes the socket to macOS, and Podman Desktop can set DOCKER_HOST for you, so docker compose commands talk to the Podman VM.
Q. How do I run a compose app at boot?
Older guides use podman generate systemd, which is deprecated. The modern way is Quadlet, covered in 68.5, or podman-compose systemd for quick setups.
Q. Why do container names differ, orders_db_1 versus orders-db-1?
podman-compose uses underscores by default and docker-compose v2 uses hyphens. Do not hardcode generated names in scripts; use service names with podman compose exec or labels instead.
Interview questions
-
How do you run a docker-compose.yml with Podman?
Model answer: podman-compose up, or enable podman.socket, set DOCKER_HOST and use docker-compose or podman compose.
-
What is the difference between podman compose and podman-compose?
Model answer: podman compose is a thin wrapper that calls an external provider; podman-compose is one such provider written in Python.
-
What usually breaks when moving compose files to Podman?
Model answer: Short image names, low ports in rootless mode and volume ownership.
-
How does Podman stay daemonless with a socket?
Model answer: The socket is user-level and socket-activated; the service exits when idle.
Quick quiz
Q1. Which tool is pure Python?
podman-compose.
Q2. What does podman compose run?
An external compose provider.
Q3. Which unit gives a Docker API?
podman.socket.
Q4. Which variable points docker-compose at Podman?
DOCKER_HOST.
Q5. Which option puts services in one pod?
--in-pod.
Samajla ka?