| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64podman-compose-1.5.0-1.el10_1.noarch |
| Applies to | Linux hosts with Podman and a Compose provider installed |
| Privilege | Rootless examples use the user Podman socket; system-wide provider paths may need administrator access |
| Scope | Three Compose paths with Podman — podman compose wrapper, Python podman-compose, and docker compose over podman.socket — provider detection and override (PODMAN_COMPOSE_PROVIDER, compose_providers), corrected pod-mode behavior on 1.5.0, service DNS, default-network and name-separator compatibility via x-podman, and a migration test matrix. Does not cover full Docker-to-Podman migration, socket hardening depth, the Compose specification reference, or Quadlet conversion workflows. |
| Related guides | Podman networking Podman vs Docker Install Podman on Rocky Linux |
You already have a compose.yaml or docker-compose.yml and want Podman to run it. The confusing part is that three different entry points exist, and podman compose is not Podman's own Compose engine — it is a thin wrapper that shells out to an external provider.
This guide maps all three paths, shows how to see which provider actually runs, and documents compatibility differences that break migrated applications — pod defaults, service DNS, network selection, and container name separators.
Three ways to run Compose with Podman
| Method | What processes the YAML | How it reaches Podman | Best fit |
|---|---|---|---|
podman compose |
External Compose provider | Podman prepares the provider environment and invokes the selected provider | Easiest Podman CLI entry point |
podman-compose |
Python podman-compose project |
Calls podman CLI directly |
No Docker API dependency; Podman-specific x-podman extensions |
docker compose / docker-compose |
Docker Compose | Docker-compatible Podman REST API via podman.socket |
Existing Docker-oriented CI or scripts |
podman compose does not parse Compose files itself. It selects a provider, sets up communication with the local Podman socket, and forwards your flags.
Check which Compose tools are installed
Before you debug networking or build failures, confirm what is on the PATH.
Look for the Python podman-compose script:
command -v podman-composeSample output when the EPEL package is installed:
/usr/bin/podman-composeCheck for the standalone docker-compose binary:
command -v docker-composeOn the RHEL 10 lab host this returned no path — docker-compose is not in default repositories here.
If docker is provided by podman-docker, check what docker compose actually resolves to:
docker compose versionSample output on this host (Podman provides the docker CLI shim):
Emulate Docker CLI using podman. Create /etc/containers/nodocker to quiet msg.
>>>> Executing external compose provider "/usr/bin/podman-compose". Please see podman-compose(1) for how to disable this message. <<<<
podman version 5.8.2
podman-compose version 1.5.0Here docker compose is not Docker Compose itself; the Podman Docker-CLI shim routed the command through podman compose, which selected /usr/bin/podman-compose. Without a separate docker-compose install, even docker compose may delegate to podman-compose through Podman's wrapper.
Ask Podman's own subcommand:
podman compose versionSample output:
>>>> Executing external compose provider "/usr/bin/podman-compose". Please see podman-compose(1) for how to disable this message. <<<<
podman version 5.8.2
podman-compose version 1.5.0The external-provider warning is evidence of which implementation is running — do not hide it when troubleshooting.
How podman compose selects a provider
podman compose
│
▼
select external provider
│
├── docker-compose (preferred when installed)
└── podman-compose (fallback)
│
▼
provider executes Compose workload against PodmanPodman's help text documents the precedence:
The default compose providers are docker-compose and podman-compose.
If installed, docker-compose takes precedence since it is the original
implementation of the Compose specification...Installing both does not make podman compose automatically mean podman-compose. If you want the Python implementation explicitly, set the provider — next section.
Override the provider with PODMAN_COMPOSE_PROVIDER
For a single shell session, point Podman at a specific binary:
export PODMAN_COMPOSE_PROVIDER=/usr/bin/podman-composeRe-run version to confirm:
podman compose versionSample output:
>>>> Executing external compose provider "/usr/bin/podman-compose". Please see podman-compose(1) for how to disable this message. <<<<
podman version 5.8.2
podman-compose version 1.5.0On hosts where docker-compose exists, you can force it the same way:
export PODMAN_COMPOSE_PROVIDER=/usr/bin/docker-composeUse this variable for temporary testing, CI jobs that must pin a provider, or comparing behavior between implementations.
Persist provider preference in containers.conf
For a lasting default, set provider order in a drop-in under /etc/containers/containers.conf.d/ or ~/.config/containers/containers.conf:
[engine]
compose_providers = [
"/usr/bin/podman-compose"
]The vendor file ships #compose_providers=[] commented out. When unset, Podman discovers providers in its built-in order. Full precedence rules for layered containers.conf files live in Podman containers.conf — this page only names which provider podman compose should try first.
Install podman-compose on RHEL-family hosts
On Enterprise Linux 10, podman-compose comes from EPEL — not base AppStream. After enabling EPEL:
sudo dnf install podman-composeConfirm the package NEVRA and script version:
podman-compose versionSample output on this host:
podman version 5.8.2
podman-compose version 1.5.0Prefer the distribution package over pip install on RHEL when EPEL provides a current build. The lab article output references podman-compose 1.5.0 with Podman 5.8.2.
Run a simple two-service application
Create a project directory with this compose.yaml:
name: basic
services:
web:
image: docker.io/library/nginx:latest
ports:
- "8080:80"
client:
image: docker.io/library/alpine:latest
command: sleep infinityStart detached:
podman-compose up -dSample output:
basic_default
basic_web_1
basic_client_1List Compose-managed services:
podman-compose psSample output:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0f2ecd467219 docker.io/library/nginx:latest /usr/bin/run-http... 32 seconds ago Up 32 seconds 0.0.0.0:8080->80/tcp, 8080/tcp, 8443/tcp basic_web_1
eef55b292d4c docker.io/library/alpine:latest sleep infinity 32 seconds ago Up 31 seconds basic_client_1Cross-check with Podman directly:
podman ps --format "table {{.Names}}\t{{.Pod}}\t{{.Networks}}"Sample output:
NAMES POD ID NETWORKS
basic_web_1 8da0c14fc17f basic_default
basic_client_1 8da0c14fc17f basic_defaultBoth services landed on the basic_default network. Generated container names use underscores: basic_web_1, not Docker Compose's hyphen style.
Default pod behavior on podman-compose 1.5.0
Older articles claim every Compose service shares localhost because podman-compose always puts them in one pod. That oversimplifies current releases.
On podman-compose 1.5.0 tested here, in_pod defaults to true in source — a pod named pod_<project> is created unless you opt out. Verify after up:
podman pod psSample output:
POD ID NAME STATUS CREATED INFRA ID # OF CONTAINERS
8da0c14fc17f pod_basic Running 33 seconds ago 2Disable pod mode when you want separate containers only:
podman-compose --in-pod=false up -dOr in YAML:
x-podman:
in_pod: falseAfter in_pod: false, podman ps shows an empty POD column and containers attach only to the Compose network — no pod_<project> resource.
Pod mode does not mean shared localhost
Default pod arguments on 1.5.0 resolve to --infra=false and --share= — no namespaces are shared unless you add them.
From the client container, localhost:80 did not reach nginx in pod mode on this host. Shared network namespace requires explicit configuration:
x-podman:
in_pod: true
pod_args:
- --share=netOnly add --share=net when you deliberately want that behavior. For normal Compose service discovery, use the service name over the project network — not 127.0.0.1.
How Compose services discover each other
On a user-defined or default Compose network, use the service name as hostname. With in_pod: false:
name: dnstest
x-podman:
in_pod: false
services:
web:
image: docker.io/library/nginx:latest
client:
image: docker.io/library/alpine:latest
command: sleep infinityAfter podman-compose up -d, resolve the web service from the client:
podman exec dnstest_client_1 getent hosts webSample output:
10.89.0.2 web.dns.podmanThe name web resolves through Podman's Compose network DNS — the model Docker Compose users expect. You do not need a shared pod or localhost for service-to-service HTTP calls.
Default network behavior differences
podman-compose and Docker Compose do not always attach services to the same network when you declare top-level networks without wiring each service.
No explicit networks block
A single-service file gets a project-scoped default network such as basic_default.
Top-level network declared but not referenced
services:
web:
image: docker.io/library/nginx:latest
networks:
backend:On podman-compose 1.5.0, the web container joined net-test_backend — the declared network — rather than an implicit unused default network. Docker Compose often still creates a default network and attaches services there when no network is listed on the service.
That mismatch breaks migrations that assume Docker's implicit default network. Enable compatibility adjustments — next section.
Enable Docker Compose compatibility extensions
podman-compose reads Podman-specific keys under x-podman. Source constants on 1.5.0 include:
docker_compose_compat— meta-setting for multiple adjustmentsdefault_net_behavior_compat— network selection closer to Docker Composename_separator_compat— hyphen instead of underscore in generated namesin_pod— pod creation toggle
Example for network behavior:
x-podman:
default_net_behavior_compat: trueOr enable the bundle:
x-podman:
docker_compose_compat: trueTest your exact compose file after toggling these — provider behavior varies by version, and not every Docker Compose edge case has a compat flag.
Container name separator difference
By default, podman-compose joins project, service, and index with underscores:
freshdemo_web_1
sep-test_web_1Docker Compose commonly emits hyphens:
freshdemo-web-1Enable hyphen compatibility:
x-podman:
name_separator_compat: trueThis matters when scripts, monitoring, or reverse-proxy configs hard-code container names. Prefer service DNS names or labels over generated container names when you can — they survive separator differences.
Run Docker Compose against the Podman socket
The third path keeps Docker Compose as the parser and uses Podman as the runtime API. This path requires an actual Docker Compose installation. If docker comes from podman-docker and no Docker Compose provider is installed, docker compose may simply route back through podman compose and invoke podman-compose instead.
Start the user socket:
systemctl --user start podman.socketPoint the Docker CLI at Podman:
export DOCKER_HOST="unix://${XDG_RUNTIME_DIR}/podman/podman.sock"Run Compose:
docker compose up -dVerify workloads with Podman, not Docker:
podman psOn this lab host without a separate docker-compose package, docker compose still printed the external-provider warning and invoked podman-compose. On a host with real Docker Compose installed, the same DOCKER_HOST pattern talks to Podman's API while using Docker's parser.
podman compose vs manual DOCKER_HOST
Both paths can end at the same Podman backend, but the setup differs.
| Step | podman compose |
Manual Docker Compose |
|---|---|---|
| Provider selection | Podman picks or honors PODMAN_COMPOSE_PROVIDER |
You choose docker compose or docker-compose |
| Socket setup | Wrapper prepares environment | You start podman.socket and export DOCKER_HOST |
| Typical provider | docker-compose if present, else podman-compose |
Whatever Compose binary you invoke |
Use podman compose when you want Podman to wire the socket. Use manual DOCKER_HOST when existing automation already calls docker compose and you only need to redirect the API endpoint.
Rootless vs rootful sockets
| Mode | Typical socket |
|---|---|
| Rootless | unix://${XDG_RUNTIME_DIR}/podman/podman.sock |
| Rootful | unix:///run/podman/podman.sock |
Whichever socket the provider uses determines which images, containers, volumes, and networks are visible. Do not mix podman-compose as your user with sudo podman ps when validating — you may be looking at a different store.
Test before you migrate production Compose files
podman-compose up -d succeeding once does not prove parity. Exercise features your file actually uses:
buildand image rebuild pathsdepends_onand health conditions- named volumes and bind mounts
- custom networks and static addresses
env_fileand variable interpolationprofilesand restart policies- secrets and configs
- GPU or device passthrough if applicable
Validate rendered config when supported:
podman-compose configThen bring the stack up and compare networks, DNS, names, and logs against Docker Compose on a staging host. Do not assume Compose-spec support is identical across providers; podman-compose 1.5.0 still has provider-specific gaps, so test features such as build secrets, volume options, profiles, and depends_on behavior individually. For example, podman-compose 1.5.0 has known gaps around volume.nocopy and some build-secret forms.
Compatibility test matrix
Run the same project through each path available on your host and fill in what you observe:
| Behavior | podman compose → podman-compose |
podman compose → docker-compose |
docker compose + DOCKER_HOST |
|---|---|---|---|
| Provider warning shown | Yes on 5.8.2 | When docker-compose installed | Depends on Compose install |
| Default network name | project_default |
Compare on your host | Compare on your host |
Service DNS (web) |
web.dns.podman |
Compare on your host | Compare on your host |
| Generated container name | project_service_1 |
Often hyphenated with docker-compose | Follows invoked Compose |
| Default pod created | Yes on 1.5.0 | N/A for docker-compose | N/A |
build |
Test | Test | Test |
The RHEL 10 lab had only podman-compose — install docker-compose separately if you need the second column.
Which Compose path should you use?
| Requirement | Starting point |
|---|---|
| Simple Podman-first workflow | podman compose after installing a provider |
No Docker API / want x-podman extensions |
podman-compose directly |
Existing docker compose CI pipelines |
DOCKER_HOST to Podman socket |
| Exact Docker Compose parser features | docker-compose provider when available |
| Long-running server after validation | Consider Podman Quadlet for systemd integration — not a drop-in for every dev loop |
Quadlet replaces Compose for some production deployments but does not replace iterative compose.yaml development on your laptop.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| External provider warning on every command | Normal for podman compose |
Read which binary follows the warning; pin with PODMAN_COMPOSE_PROVIDER if needed |
Expected podman-compose but got docker-compose |
Provider precedence | Uninstall or reorder compose_providers; export PODMAN_COMPOSE_PROVIDER |
| Service name does not resolve | Wrong network or pod assumptions | Check podman network inspect; use service DNS, not localhost |
localhost between services fails |
Default pod does not share net namespace | Add pod_args: [--share=net] only if you need it; prefer service names |
| Container names differ from Docker | Underscore vs hyphen | Set name_separator_compat: true or stop hard-coding names |
docker compose still runs podman-compose |
No docker-compose package installed | Install Docker Compose or call podman-compose explicitly |
| Rootless vs rootful mismatch | Different sockets | Align user, DOCKER_HOST, and podman ps context |
References
- podman-compose(1) — Podman wrapper subcommand
- podman-compose project — Python implementation and
x-podmankeys - containers.conf(5) —
compose_providers - Red Hat — Podman Compose or Docker Compose — official positioning
- Compose specification — portable compose file format
Summary
Running Compose on Podman means choosing an entry point, not picking one magical command. podman compose is a wrapper that delegates to docker-compose or podman-compose; on the RHEL 10 lab with only EPEL podman-compose 1.5.0 installed, every path printed the external-provider warning and invoked that Python implementation.
podman-compose 1.5.0 defaults in_pod to true, creating a pod_<project> resource, but default pod arguments do not share network namespaces — service names resolve over Compose network DNS such as web.dns.podman, and localhost between containers fails unless you explicitly set pod_args with --share=net. Disable pods with --in-pod=false or x-podman.in_pod: false when you want standalone containers.
For migrations, test networks, generated names, builds, and dependencies with the same file on each provider you plan to support. Pin providers through PODMAN_COMPOSE_PROVIDER or compose_providers when CI must not drift. Docker-oriented teams can keep docker compose and point DOCKER_HOST at podman.socket — the runtime is Podman even when the CLI says Docker.

