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 a generated/etc/hoststhe script owns. - 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.
compose2pod's generated scripts own /etc/hosts: they write it to a temp
file and bind-mount it read-only into every container under --no-hosts
(see architecture/supported-subset.md), so pod-internal name resolution
works on any Podman version. host.containers.internal /
host.docker.internal are not provided — add an explicit extra_hosts
entry if you need them.
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.