Running Compose Projects With Podman
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.
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
- Enable the API socket and point Docker Compose at it. On Linux, the user-level systemd socket provides it; on macOS,
podman machineexposes 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}}'
- 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.
- 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
- 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-idmaps 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.
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
- 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"
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.Record the runtime in the doctor output so support questions start with the right assumptions.
Platform caveats
macOS:
podman machineruns a Fedora CoreOS VM with virtiofs mounts on recent versions. The machine is rootless inside by default;podman machine set --rootfulswitches 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 machinesupports Rosetta for amd64 images from Podman 5.1; enable it incontainers.confwithrosetta=trueunder[machine].
SELinux (Fedora, RHEL): bind mounts need the
:Zor:zsuffix to relabel content, or containers getpermission deniedeven 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.