CertGrid CertGrid
Hands-on Lab·Podman

Docker Compose with Podman

Enable `podman.socket`, set `DOCKER_HOST`, and the genuine Docker Compose binary brings up a two-service stack on Podman. Then `docker compose down` hits the rootless netns error, exits 1, and leaves a container behind.

Registries and Remote Hosts Guide 36 of 47 Intermediate

Written against the versions above. Podman follows the distribution here rather than a vendor repository, so the version you get is the one Ubuntu shipped. The commands are stable across 5.x.

Every command on this page ran on podman01.
Server NameIP AddressOSRolesCPURAMHDD
PODMAN01192.168.0.24Ubuntu 26.04 LTSPrimary Container Host2 Core4 GB50 GB

Before you start

  1. Nothing is listening yet

    podman.socket is disabled and the socket file does not exist. This is the state guide 1 found immediately after installing, and it is deliberate - a Docker-compatible API on by default would undo half the point of being daemonless.

    It is worth being clear about what enabling it does and does not mean. The socket is socket-activated: systemd holds the listening socket, and podman system service is started only when something connects and exits again afterwards. So you are not starting a daemon - you are agreeing that one may be started on demand.

    bash Example session
    systemctl --user is-enabled podman.socketdisabled[exit 1]ls -l $XDG_RUNTIME_DIR/podman/podman.sockls: cannot access '/run/user/1000/podman/podman.sock': No such file or directory[exit 2]

    Expected resultdisabled, and No such file or directory.

    Success conditionYou have confirmed there is no API socket before you create one.

  2. Enable it, and prove it speaks Docker

    systemctl --user enable --now podman.socket and the socket appears at $XDG_RUNTIME_DIR/podman/podman.sock - under /run/user/1000, owned by you. Rootful Podman puts it at /run/podman/podman.sock instead, and which one a tool finds decides which Podman it drives.

    The proof that it is Docker-compatible is to talk to it as Docker would:

    curl --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock http://d/v1.41/version

    A Docker API path - /v1.41/version - answered by Podman. That is the whole compatibility story: not a translation layer around the CLI as in guide 4, but an implementation of Docker's HTTP API. Which is why it works for tools rather than only for people typing commands.

    bash Example session
    systemctl --user enable --now podman.socketCreated symlink '/home/sysadmin/.config/systemd/user/sockets.target.wants/podman.socket' → '/usr/lib/systemd/user/podman.socket'.systemctl --user is-active podman.socketactivels -l $XDG_RUNTIME_DIR/podman/podman.sockls: cannot access '/run/user/1000/podman/podman.sock': No such file or directory[exit 2]curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock http://d/v1.41/version{"Platform":{"Name":"linux/amd64/ubuntu-26.04"},"Components":[{"Name":"Podman Engine","Version":"5.7.0","Details":{"APIVersion":"5.7.0","Arch":"amd64","BuildTime":"2026-02-23T15:35:32Z","Experimental":"false","GitCommit":"","GoVersion":"go1.25.0","KernelVersion":"7.0.0-30-generic","MinAPIVersion":"4.0.0","Os":"linux"}},{"Name":"Conmon","Version":"conmon version 2.1.13, commit: unknown","Details":{"Package":"conmon_2.1.13+ds1-2_amd64"}},{"Name":"OCI Runtime (crun)","Version":"crun version 1.21\ncommit: 10269840aa07fb7e6b7e1acff6198692d8ff5c88\nrundir: /run/user/1000/crun\nspec: 1.0.0\n+SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +CRIU +WASM:wasmedge +YAJL","Details":{"Package":"crun_1.21-1ubuntu3_amd64"}}],"Version":"5.7.0","ApiVersion":"1.41","MinAPIVersion":"1.24","GitCommit":"","GoVersion":"go1.25.0","Os":"linux","Arch":"amd64","KernelVersion":"7.0.0-30-generic","BuildTime":"2026-02-23T15:35:32Z"}

    Expected resultAn active socket unit, a socket file you own, and a JSON version response from a Docker API path.

    Success conditionA Docker API request was answered by Podman.

  3. Point Compose at it

    An ordinary compose.yaml - two services, a published port, a named volume. Nothing Podman-specific in it.

    One environment variable connects the two:

    DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock docker compose up -d

    And it works exactly as it would against Docker - creating a network, a volume and two containers, in dependency order:

     Network stack_default  Created
     Volume stack_site      Created
     Container stack-web-1  Started

    Then check with Podman's commands. podman ps lists stack-web-1 and stack-cache-1; podman volume ls shows stack_site. Compose created real Podman objects, and both tools see the same things - because there is one store, not a Docker-compatible shadow copy.

    curl gets http=200. And docker compose ps reports the stack as Compose understands it, so Compose's own state tracking works too.

    bash Example session
    cd ~/stack && cat compose.yamlservices:  web:    image: docker.io/library/nginx:alpine    ports:      - "8093:80"    volumes:      - site:/usr/share/nginx/html  cache:    image: docker.io/library/redis:alpinevolumes:  site:cd ~/stack && DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock docker compose up -d>>>> Executing external compose provider "/usr/libexec/docker/cli-plugins/docker-compose". Please see podman-compose(1) for how to disable this message. <<<<  Network stack_default  Creating Network stack_default  Created Volume stack_site  Creating Volume stack_site  Created Container stack-cache-1  Creating Container stack-web-1  Creating Container stack-cache-1  Created Container stack-web-1  Created Container stack-cache-1  Starting Container stack-web-1  Starting Container stack-cache-1  Started Container stack-web-1  Startedpodman ps --format 'table {{.Names}} {{.Image}} {{.Status}}'NAMES          IMAGE                           STATUSstack-cache-1  docker.io/library/redis:alpine  Up Less than a secondstack-web-1    docker.io/library/nginx:alpine  Up Less than a secondpodman volume lsDRIVER      VOLUME NAMElocal       stack_sitecurl -s -o /dev/null -w 'http=%{http_code}\n' http://localhost:8093http=200cd ~/stack && DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock docker compose ps>>>> Executing external compose provider "/usr/libexec/docker/cli-plugins/docker-compose". Please see podman-compose(1) for how to disable this message. <<<< NAME            IMAGE                            COMMAND                  SERVICE   CREATED         STATUS        PORTSstack-cache-1   docker.io/library/redis:alpine   "redis-server"           cache     2 seconds ago   Up 1 secondstack-web-1     docker.io/library/nginx:alpine   "nginx -g daemon off;"   web       2 seconds ago   Up 1 second   80/tcp

    Expected resultCompose creating a network, volume and two containers, all visible to podman ps.

    Success conditionA Compose file is running under Podman.

  4. And the teardown is where it shows

    docker compose down does not come back clean:

     Container stack-cache-1  Error while Stopping
    Error response from daemon: removing container ... network:
      * rootless netns: kill network process: permission denied
    Error: executing /usr/libexec/docker/cli-plugins/docker-compose down:
      exit status 1

    Same rootless netns failure as guide 23, and worse here. podman kube down printed the error and finished the job; Compose exits 1 and podman ps -a still shows stack-cache-1 behind. The teardown genuinely did not complete.

    That is the honest summary of this compatibility route: the API is good enough that real tools work, and the seams show at teardown. For interactive use it is fine - re-run down, or podman rm -af. For a CI pipeline that checks exit codes it is a problem, and the fix is not to use Compose there: a Quadlet unit (guide 40) or podman kube play is what to reach for when something has to be scripted.

    Also note DOCKER_HOST had to be set on every invocation. Export it in your shell profile if you use this regularly, and remember it makes every Docker-speaking tool on the machine talk to Podman - which is usually what you want and occasionally a surprise.

    bash Example session
    cd ~/stack && DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock docker compose down>>>> Executing external compose provider "/usr/libexec/docker/cli-plugins/docker-compose". Please see podman-compose(1) for how to disable this message. <<<<  Container stack-web-1  Stopping Container stack-cache-1  Stopping Container stack-web-1  Stopped Container stack-web-1  Removing Container stack-cache-1  Error while Stopping Container stack-web-1  RemovedError response from daemon: removing container 2b34bcc8c996b9f5cde6d6de2cf6d6745f7a0d4e08d6a97d7d1a686398fa80c6 network: 1 error occurred:	* rootless netns: kill network process: permission deniedError: executing /usr/libexec/docker/cli-plugins/docker-compose down: exit status 1[exit 1]podman ps -a --format 'table {{.Names}} {{.Status}}'NAMES          STATUSstack-cache-1  Exited (0) Less than a second ago

    Expected resultA stopping error, exit status 1, and one container still listed afterwards.

    Success conditionYou know what this route does badly as well as what it does well.

Troubleshooting

Official sources