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 or 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, with no dependencies and no compiled wheels, so it installs and runs in minimal Python images.
Podman 4.9 or newer. compose2pod accepts only forms that every Podman from 4.9 to 6.1 can run, so a script it generates runs on any of them (ADR-0006).
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, 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 'compose2pod[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.shOptions:
--target: the service to run in the foreground (required).--image: the CI image that replaces every service with abuildsection (required).--command: a shell command overriding the target service's command.--project-dir: the host path that relative volume andenv_filesources resolve against (default.).--pod-name: the name of the Podman pod, also used as the prefix of every container name (defaulttest-pod).--format: the input format, one ofauto,json,yaml(defaultauto, which tries JSON, then YAML).--artifact SRC:DST: a file topodman cpout of the target container after it exits (repeatable).--allow-exit-code: a target exit code treated as success in addition to 0 (repeatable).
compose2pod refuses every document docker compose config refuses. A test
harness runs docker compose config and compose2pod over the same files to
check this. So a file that compiles is a file Docker would run; where compose2pod
still refuses a form Docker accepts, it is because Podman cannot express it;
each case is documented in docs/adr/.
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 thebindandtmpfsoption maps),tmpfs,healthcheck,depends_on(all conditions),links(read as a dependency plus a hostname alias, as Docker reads it), networkaliases,hostname/container_name. - Confinement and 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.
Accepted and ignored with a warning, since they mean nothing inside one shared pod:
ports, expose, restart, stdin_open, tty, stop_signal,
stop_grace_period, and profiles (every service runs regardless of profile).
Compose extension fields (any x--prefixed key) and YAML anchors are accepted
as-is, so a top-level x-* anchor block for shared config is accepted.
${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). The boundary rulings, which forms are refused and why,
are recorded in docs/adr/.
Beta. Part of the modern-python family. MIT licensed.