A Compose file that works on Docker fails on Podman in ways that look unrelated: Error: short-name "postgres" did not resolve to an alias, rootlessport cannot expose privileged port 80, permission denied when Postgres writes to its data directory, or depends_on with condition: service_healthy being ignored. Podman is a good runtime — daemonless, rootless by default, and free under the Apache licence — but it is not Docker, and running a Docker-shaped stack on it needs a handful of explicit adjustments. This page lists them, as part of choosing and tuning a local container runtime.

There are two ways to run Compose files on Podman: the Python podman-compose tool, which translates Compose into Podman commands and has gaps in newer features, and the real Docker Compose v2 binary talking to Podman's Docker-compatible API socket. The second gives much closer behaviour to Docker, so this page uses it throughout.

Diagnostic

Check which Compose implementation is running, where the socket is, and whether the machine is rootless:

#!/usr/bin/env bash
set -euo pipefail
podman version --format 'client={{.Client.Version}} server={{.Server.Version}}'
podman info --format 'rootless={{.Host.Security.Rootless}} socket={{.Host.RemoteSocket.Path}}'
docker compose version 2>/dev/null || echo "docker compose v2 not installed"
podman compose version 2>&1 | head -2
docker compose up -d 2>&1 | tail -5 || true

Expected bad output on a fresh setup:

client=5.2.2 server=5.2.2
rootless=true socket=/run/user/1000/podman/podman.sock
docker compose v2 not installed
>>>> Executing external compose provider "/usr/bin/podman-compose". <<<<
Error: short-name "postgres:16" did not resolve to an alias and no unqualified-search registries are defined

podman compose delegated to podman-compose, and the image reference postgres:16 failed because Podman does not assume Docker Hub for unqualified names.

Which Podman Compose Path Is in Use Decision diagram showing how podman compose picks a provider and the consequences of each. Which Podman Compose Path Is in Use Which provider runs the file? podman-compose translation, feature gaps docker compose v2 real Compose, Podman API
Pointing Docker Compose v2 at the Podman socket gives the closest match to Docker behaviour.

Root cause

Podman deliberately differs from Docker in four ways that matter to Compose. It does not default unqualified image names to docker.io, for supply-chain safety, so postgres:16 is ambiguous. It runs rootless by default, so containers cannot bind host ports below 1024 and files created in bind mounts are owned by a subordinate UID on the host rather than your user. It has no long-running daemon, so the Docker-compatible API is a socket activated by systemd that must be enabled explicitly. And podman compose is only a wrapper that picks whichever provider it finds first — often podman-compose, which implements a subset of the Compose specification and handles health-gated depends_on differently. None of this is broken; it is just not what a Docker-authored file expects.

Resolution

  1. Enable the API socket and point Docker Compose at it. On Linux, the user-level systemd socket provides it; on macOS, podman machine exposes it automatically.
#!/usr/bin/env bash
set -euo pipefail
if [ "$(uname -s)" = Linux ]; then
  systemctl --user enable --now podman.socket
  export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
else
  podman machine init --cpus 4 --memory 6144 --disk-size 80 || true
  podman machine start
  export DOCKER_HOST="unix://$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')"
fi
docker compose version
docker info --format '{{.OperatingSystem}}'
  1. Fully qualify image names so no alias lookup is needed. This change is harmless on Docker and makes the file unambiguous everywhere:
services:
  db:
    image: docker.io/library/postgres:16.4
  cache:
    image: docker.io/library/redis:7.4
  mail:
    image: docker.io/axllent/mailpit:v1.20

If editing every file is not possible, add unqualified-search-registries = ["docker.io"] to ~/.config/containers/registries.conf on each machine — but that is per-machine state, so qualifying names in the repository is the durable fix.

  1. Handle privileged ports. Rootless Podman cannot bind ports below 1024. Either publish unprivileged ports (8443:443) or lower the threshold once per Linux machine:
#!/usr/bin/env bash
set -euo pipefail
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-rootless-ports.conf
sudo sysctl --system | grep unprivileged
  1. Fix bind-mount ownership. In rootless mode, UID 0 inside the container maps to your user, but other container UIDs map to subordinate IDs. Services that run as a non-root user (Postgres as 999, Node images as 1000) then create files your host user cannot edit. userns_mode: keep-id maps your host UID to the same UID inside the container:
services:
  web:
    image: docker.io/library/node:20-bookworm
    userns_mode: keep-id
    user: "${UID:-1000}:${GID:-1000}"
    volumes:
      - ./:/app

Use named volumes, not bind mounts, for database data directories; Podman manages their ownership and the problem disappears. The general UID-mapping approach is covered in matching container UID and GID to the host user.

Podman Adjustments by Layer Layered list of the four changes needed to run a Docker Compose file on Podman. Podman Adjustments by Layer API socket podman.socket, DOCKER_HOST Image names fully qualified docker.io/... Host ports 1024+ or sysctl threshold Bind mounts keep-id or named volumes
Only the image names change the repository; the rest is per-machine setup the bootstrap script performs.

Expected output

$ docker compose up -d
[+] Running 4/4
 ✔ Network shop_default   Created
 ✔ Container shop-db-1     Healthy
 ✔ Container shop-cache-1  Started
 ✔ Container shop-api-1    Started
$ docker compose ps --format '{{.Service}} {{.State}} {{.Health}}'
api running
cache running
db running healthy

The real Compose v2 honours depends_on: condition: service_healthy, so api starts only after db reports healthy — the same ordering Docker gives. Files the web service writes into the bind mount are owned by your host user.

Prevention

  1. Qualify every image in CI. A lint step catches unqualified names before they reach a Podman user:
#!/usr/bin/env bash
set -euo pipefail
docker compose config --format json | jq -r '.services[].image // empty' \
  | grep -vE '^[a-z0-9.-]+\.[a-z]{2,}(:[0-9]+)?/' && { echo "unqualified image names above"; exit 1; } || echo "all images qualified"
  1. Run CI once on Podman. A nightly job that brings the stack up on a rootless Podman runner catches Docker-only assumptions — privileged ports, root-owned bind mounts, network_mode: host — the week they are introduced.

  2. Record the runtime in the doctor output so support questions start with the right assumptions.

podman-compose vs Docker Compose on Podman Comparison of the two ways to run Compose files on Podman across compatibility features. podman-compose vs Docker Compose on Podman podman-compose docker compose v2 Python translation layer Compose spec reference partial profiles, include full profiles, include healthy condition varies healthy condition honoured no compose watch compose watch works
The real Compose binary reduces Podman-specific differences to the runtime itself.

Platform caveats

macOS: podman machine runs a Fedora CoreOS VM with virtiofs mounts on recent versions. The machine is rootless inside by default; podman machine set --rootful switches it and removes the port and ownership differences at the cost of the security benefit.

WSL2: Podman Desktop on Windows creates its own WSL distribution. Run Compose from Windows against its socket, or install Podman inside your development distribution — not both, or two separate image stores diverge.

Apple Silicon (ARM64): podman machine supports Rosetta for amd64 images from Podman 5.1; enable it in containers.conf with rosetta=true under [machine].

SELinux (Fedora, RHEL): bind mounts need the :Z or :z suffix to relabel content, or containers get permission denied even as root. Docker ignores the suffix, so it is safe to add in shared Compose files.

Rollback

Unset DOCKER_HOST or switch the context back to the Docker engine; qualified image names keep working on Docker unchanged:

#!/usr/bin/env bash
set -euo pipefail
unset DOCKER_HOST
docker context use default
docker compose up -d

Frequently Asked Questions

Should we use podman-compose or Docker Compose with Podman?

Use Docker Compose v2 against Podman's API socket unless the organisation forbids installing it. It implements the full Compose specification, including profiles, include, watch and health-gated depends_on, which podman-compose supports only partially.

Why do files created by a container show an odd owner like 100999?

In rootless mode, container UIDs other than 0 map to a subordinate UID range on the host. A process running as UID 1000 inside may create files owned by 100999 outside. Use userns_mode: keep-id for development services that write into bind mounts.

Does host.docker.internal work on Podman?

Podman provides host.containers.internal and, on recent versions, also resolves host.docker.internal. On older versions add extra_hosts: ["host.docker.internal:host-gateway"] to the service.

Can Testcontainers use Podman?

Yes, through the same Docker-compatible socket. Set DOCKER_HOST to the Podman socket and TESTCONTAINERS_RYUK_DISABLED=true if the Ryuk reaper cannot mount the socket in rootless mode, then clean up containers in test teardown instead.