Podman Compose: Run Docker Compose Files with Podman

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
podman-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:

bash
command -v podman-compose

Sample output when the EPEL package is installed:

output
/usr/bin/podman-compose

Check for the standalone docker-compose binary:

bash
command -v docker-compose

On 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:

bash
docker compose version

Sample output on this host (Podman provides the docker CLI shim):

output
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.0

Here 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:

bash
podman compose version

Sample output:

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.0

The external-provider warning is evidence of which implementation is running — do not hide it when troubleshooting.


How podman compose selects a provider

text
podman compose
select external provider
      ├── docker-compose   (preferred when installed)
      └── podman-compose   (fallback)
provider executes Compose workload against Podman

Podman's help text documents the precedence:

text
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:

bash
export PODMAN_COMPOSE_PROVIDER=/usr/bin/podman-compose

Re-run version to confirm:

bash
podman compose version

Sample output:

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.0

On hosts where docker-compose exists, you can force it the same way:

bash
export PODMAN_COMPOSE_PROVIDER=/usr/bin/docker-compose

Use 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:

toml
[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:

bash
sudo dnf install podman-compose

Confirm the package NEVRA and script version:

bash
podman-compose version

Sample output on this host:

output
podman version 5.8.2
podman-compose version 1.5.0

Prefer 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:

yaml
name: basic
services:
  web:
    image: docker.io/library/nginx:latest
    ports:
      - "8080:80"

  client:
    image: docker.io/library/alpine:latest
    command: sleep infinity

Start detached:

bash
podman-compose up -d

Sample output:

output
basic_default
basic_web_1
basic_client_1

List Compose-managed services:

bash
podman-compose ps

Sample output:

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_1

Cross-check with Podman directly:

bash
podman ps --format "table {{.Names}}\t{{.Pod}}\t{{.Networks}}"

Sample output:

output
NAMES           POD ID        NETWORKS
basic_web_1     8da0c14fc17f  basic_default
basic_client_1  8da0c14fc17f  basic_default

Both 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:

bash
podman pod ps

Sample output:

output
POD ID        NAME        STATUS      CREATED         INFRA ID    # OF CONTAINERS
8da0c14fc17f  pod_basic   Running     33 seconds ago              2

Disable pod mode when you want separate containers only:

bash
podman-compose --in-pod=false up -d

Or in YAML:

yaml
x-podman:
  in_pod: false

After 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:

yaml
x-podman:
  in_pod: true
  pod_args:
    - --share=net

Only 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:

yaml
name: dnstest
x-podman:
  in_pod: false
services:
  web:
    image: docker.io/library/nginx:latest
  client:
    image: docker.io/library/alpine:latest
    command: sleep infinity

After podman-compose up -d, resolve the web service from the client:

bash
podman exec dnstest_client_1 getent hosts web

Sample output:

output
10.89.0.2       web.dns.podman

The 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

yaml
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 adjustments
  • default_net_behavior_compat — network selection closer to Docker Compose
  • name_separator_compat — hyphen instead of underscore in generated names
  • in_pod — pod creation toggle

Example for network behavior:

yaml
x-podman:
  default_net_behavior_compat: true

Or enable the bundle:

yaml
x-podman:
  docker_compose_compat: true

Test 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:

text
freshdemo_web_1
sep-test_web_1

Docker Compose commonly emits hyphens:

text
freshdemo-web-1

Enable hyphen compatibility:

yaml
x-podman:
  name_separator_compat: true

This 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:

bash
systemctl --user start podman.socket

Point the Docker CLI at Podman:

bash
export DOCKER_HOST="unix://${XDG_RUNTIME_DIR}/podman/podman.sock"

Run Compose:

bash
docker compose up -d

Verify workloads with Podman, not Docker:

bash
podman ps

On 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:

  • build and image rebuild paths
  • depends_on and health conditions
  • named volumes and bind mounts
  • custom networks and static addresses
  • env_file and variable interpolation
  • profiles and restart policies
  • secrets and configs
  • GPU or device passthrough if applicable

Validate rendered config when supported:

bash
podman-compose config

Then 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 composepodman-compose podman composedocker-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


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.


Frequently Asked Questions

1. What is the difference between podman compose and podman-compose?

podman compose is a Podman subcommand that delegates to an external Compose provider such as docker-compose or podman-compose. podman-compose is the Python implementation that calls the Podman CLI directly. They are not the same program.

2. Which provider does podman compose use by default?

When both are installed, Podman prefers docker-compose over podman-compose. On hosts with only podman-compose installed, podman compose invokes podman-compose and prints an external provider warning. Override with PODMAN_COMPOSE_PROVIDER or compose_providers in containers.conf.

3. Do podman-compose services share localhost by default?

No. On podman-compose 1.5.0, services default into a shared pod, but default pod_args use --infra=false and --share= with no namespaces shared. Service names resolve over the Compose network DNS instead. Use x-podman.pod_args with --share=net only when you deliberately want shared network namespace behavior.

4. Can I run docker compose against Podman?

Yes. Start podman.socket, set DOCKER_HOST to the rootless or rootful Podman socket, and run docker compose or docker-compose. The Compose tool talks to Podman's Docker-compatible API, not Docker Engine.

5. How do I disable pod mode in podman-compose?

Pass --in-pod=false on the command line or set x-podman.in_pod false in the compose file. On 1.5.0 the default is in_pod true, which creates a pod_projectname pod unless you opt out.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)