Convert a Docker Compose file into a POSIX sh script that runs its services as a single Podman pod.
Built for CI and test environments where you can't use docker compose or podman kube play:
- No bridge networking / netavark. Unprivileged CI containers often have a read-only
/proc/sys, so netavark fails to create bridge networks. A single pod shares one network namespace with no bridge: services talk over127.0.0.1, and names resolve via--add-host. - No systemd. Podman healthchecks are normally scheduled by systemd timers. compose2pod gates startup by polling
podman healthcheck rundirectly, sodepends_on: service_healthyworks without systemd. - No heavy runtime. The core is stdlib-only — no dependencies, no compiled wheels — so it installs and runs in minimal Python images.
Podman >= 6.0.0. Earlier releases have a bug where a container stopping
inside a multi-container pod wipes /etc/hosts for every container in that
pod, not just the one that stopped — fixed in 6.0.0. compose2pod's generated
scripts rely on one shared --add-host-populated /etc/hosts for the whole
pod (see architecture/supported-subset.md), so a service_completed_successfully
dependency (a container that runs and exits, e.g. a migration step) can wipe
name resolution for everything started after it on a pre-6.0.0 Podman.
pip install compose2pod # core: reads compose as JSON
pip install compose2pod[yaml] # optional: read YAML directly (adds PyYAML)# YAML directly (needs the [yaml] extra)
compose2pod docker-compose.yml --target app --image myimage:ci > run.sh
# Or stay dependency-free by piping JSON (e.g. via yq)
yq -o=json '.' docker-compose.yml | compose2pod --target app --image myimage:ci > run.sh
sh ./run.shcompose2pod refuses every document docker compose config refuses — a
measured property, checked continuously by a differential conformance harness
that runs the real Docker CLI and the real compose2pod pipeline over the same
YAML. So a file that compiles is a file Docker would run; where compose2pod
still refuses a form Docker accepts, it is because Podman genuinely cannot
express it (each such case is documented, not guessed).
Within that boundary it covers most of what real compose files use:
- Services —
image/build,command/entrypoint,environmentandenv_file(string and long-form{path, required, format}),volumes(short-form and long-form--mount, including thebind/volume/tmpfsoption maps),tmpfs,healthcheck,depends_on(all conditions), networkaliases,hostname/container_name. - Confinement & metadata —
user,working_dir,read_only,init,privileged,cap_add/cap_drop,security_opt,devices,group_add,platform,labels,annotations,pull_policy(the quoted-boolean and YAML-1.1 spellings Docker accepts, too). - Resources — the legacy keys (
mem_limit,cpus,pids_limit,ulimits, …) and the moderndeploy.resourcesblock. - Pod-wide —
dns/dns_search/dns_opt,sysctls,extra_hosts. - Composition — same-file
extends,secrets/configs,profiles.
Compose extension fields (any x--prefixed key) and YAML anchors are accepted
as-is, so a top-level x-* anchor block for shared config just works.
${VAR}-style variable interpolation is left live in the generated script,
resolved by its shell against the environment present when the script runs (no
.env file support). See architecture/supported-subset.md for the full
accept/ignore/reject matrix and planning/decisions/ for the boundary rulings.
Beta. Part of the modern-python family. MIT licensed.