TL;DR — Quick Summary

Complete guide to Podman Compose for rootless container orchestration. Install, configure, and migrate from Docker Compose with systemd integration and Quadlet.

PODMAN COMPOSE — ROOTLESS ORCHESTRATION podman pod (rootless user namespace) app (Node.js) port: 3000 depends_on: db healthcheck: /health db (PostgreSQL) port: 5432 volume: pgdata:Z healthcheck: pg_isready redis (Cache) port: 6379 volume: redis-data healthcheck: ping systemd Quadlet auto-start user units No root daemon — containers run as your user via Linux namespaces

Podman Compose brings the familiar Docker Compose workflow to the daemonless, rootless Podman container engine. If you have been looking for a Docker alternative that does not require a privileged background daemon, exposes no root attack surface, and integrates natively with systemd, this guide is for you. You will learn how to install Podman and podman-compose, configure rootless user namespaces, migrate existing Docker Compose projects, manage pods, and enable auto-start through Quadlet — the modern systemd-native approach.

Prerequisites

Before you begin, make sure you have:

  • A Linux system: Ubuntu 22.04/24.04, Fedora 39+, or RHEL/Rocky Linux 9
  • A regular user account (root is not required for day-to-day operations)
  • Basic familiarity with Docker Compose YAML syntax
  • sudo access for the initial installation only

Why Podman Instead of Docker?

Docker’s architecture centers on a privileged daemon (dockerd) running as root. Every docker CLI call goes through a Unix socket owned by root. This design means any process that can reach the Docker socket effectively has root on the host — a significant security concern for shared systems and CI runners.

Podman takes a fundamentally different approach:

AspectDockerPodman
ArchitectureClient-server daemonFork/exec, daemonless
Default privilegeRuns as rootRootless by default
Compose supportdocker compose pluginpodman-compose or podman compose
Image formatOCI (Docker-compatible)OCI (Docker-compatible)
Socket requirement/var/run/docker.sock (root)Optional user socket
systemd integrationExternal unit filesNative Quadlet support
CLI compatibilityReference implementationDrop-in (alias docker=podman)

Podman containers are spawned as child processes of the calling user. There is no daemon to exploit, no always-running root process, and container images are stored per-user under ~/.local/share/containers/.

Installation

Ubuntu 22.04 / 24.04

sudo apt update
sudo apt install -y podman
podman --version
# podman version 4.x.x or 5.x.x

For Podman 5.x on Ubuntu 22.04, add the official Kubic repository:

. /etc/os-release
echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_${VERSION_ID}/ /" \
  | sudo tee /etc/apt/sources.list.d/kubic-podman.list
curl -fsSL "https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_${VERSION_ID}/Release.key" \
  | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/kubic-podman.gpg
sudo apt update && sudo apt install -y podman

Fedora / RHEL / Rocky Linux

Podman is the default container engine on these distributions:

# Fedora
sudo dnf install -y podman

# RHEL 9 / Rocky Linux 9
sudo dnf install -y container-tools

Optional: Drop-in Docker Alias

Install podman-docker to redirect all docker CLI commands to Podman:

# Ubuntu
sudo apt install -y podman-docker

# Fedora/RHEL
sudo dnf install -y podman-docker

After this, docker run, docker build, and docker ps all invoke Podman transparently.

Configuring Rootless Containers

Rootless operation depends on Linux user namespaces and UID/GID mappings defined in /etc/subuid and /etc/subgid.

Verify subuid/subgid Configuration

grep "^$(whoami)" /etc/subuid /etc/subgid
# Expected output (one line per file):
# /etc/subuid: youruser:100000:65536
# /etc/subgid: youruser:100000:65536

If the entries are missing, add them:

sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
podman system migrate

Verify Rootless Functionality

podman run --rm hello-world
# Should print "Hello from Docker!" without sudo

slirp4netns / pasta Networking

Rootless containers use slirp4netns (older) or pasta (Podman 5+) for user-space networking. This allows port binding above 1024 without root. For ports below 1024:

# Allow unprivileged binding of port 80 (system-wide)
echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-unprivileged-ports.conf
sudo sysctl --system

Installing podman-compose

Via pip (all distributions)

pip install --user podman-compose
# Ensure ~/.local/bin is in your PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
podman-compose --version

Via package manager

# Fedora / RHEL
sudo dnf install -y podman-compose

# Ubuntu (may be older version)
sudo apt install -y podman-compose

Built-in podman compose

Podman 4.4+ includes a built-in podman compose subcommand that delegates to an installed docker-compose or podman-compose binary automatically:

podman compose version

Writing a compose.yaml for Podman

Podman Compose supports the Compose Specification (v2/v3 syntax). Your existing docker-compose.yml files work without modification in the vast majority of cases.

Here is a production-ready full-stack example — a Node.js API backed by PostgreSQL and Redis:

# compose.yaml
name: myapp

services:
  api:
    build:
      context: ./api
      dockerfile: Containerfile
    image: myapp-api:latest
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: "postgresql://appuser:${DB_PASSWORD}@db:5432/appdb"
      REDIS_URL: "redis://redis:6379"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s
    volumes:
      - ./api/logs:/app/logs:Z
    networks:
      - backend
    restart: unless-stopped

  db:
    image: docker.io/library/postgres:16-alpine
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: "${DB_PASSWORD}"
    volumes:
      - pgdata:/var/lib/postgresql/data:Z
      - ./db/init:/docker-entrypoint-initdb.d:ro,Z
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    networks:
      - backend
    restart: unless-stopped

  redis:
    image: docker.io/library/redis:7-alpine
    command: redis-server --save 60 1 --loglevel warning
    volumes:
      - redis-data:/data:Z
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - backend
    restart: unless-stopped

networks:
  backend:
    driver: bridge

volumes:
  pgdata:
  redis-data:

Key Podman-specific notes in this file:

  • docker.io/library/ prefix: Podman does not add Docker Hub as an implicit registry. Always use fully qualified image references to avoid ambiguity.
  • :Z volume label: On SELinux-enabled systems (Fedora, RHEL, Rocky), the :Z suffix relabels the volume so the container process can access it. Use :z (lowercase) for shared volumes accessed by multiple containers.
  • name: at the top: Sets the Compose project name, which becomes the pod name prefix.

Start the stack:

podman-compose up -d
podman pod ps
podman ps
podman-compose logs -f api

Managing Pods

When podman-compose starts a multi-service stack, it creates a Podman pod that groups the containers and shares a network namespace:

# List all pods
podman pod ps

# Inspect a pod
podman pod inspect myapp_default

# Real-time resource usage
podman pod stats myapp_default

# Stop and remove pod + containers
podman-compose down

# Remove volumes too
podman-compose down -v

You can also manage pods directly:

# Create a pod manually
podman pod create --name mypod -p 3000:3000 -p 5432:5432

# Run a container inside an existing pod
podman run -d --pod mypod --name db postgres:16-alpine

Building Images with Buildah and Containerfiles

Podman uses Buildah under the hood for image builds. Use a Containerfile (identical syntax to Dockerfile) for clarity:

# Containerfile
FROM docker.io/library/node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

FROM docker.io/library/node:22-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
RUN adduser -D -u 1001 appuser
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "server.js"]

Build and push:

podman build -t myapp-api:latest -f Containerfile ./api
podman push myapp-api:latest ghcr.io/yourorg/myapp-api:latest

systemd Integration: Quadlet

Quadlet is the modern way to run Podman containers as systemd services without writing unit files manually. It ships with Podman 4.4+.

Per-User Quadlet (Rootless)

Create ~/.config/containers/systemd/myapp.pod:

[Unit]
Description=MyApp Pod

[Pod]
PodName=myapp
PublishPort=3000:3000

[Install]
WantedBy=default.target

Create ~/.config/containers/systemd/myapp-db.container:

[Unit]
Description=MyApp Database
After=myapp-pod.service

[Container]
Image=docker.io/library/postgres:16-alpine
Pod=myapp.pod
Environment=POSTGRES_DB=appdb
Environment=POSTGRES_USER=appuser
EnvironmentFile=%h/.config/myapp/db.env
Volume=%h/.local/share/myapp/pgdata:/var/lib/postgresql/data:Z
HealthCmd=pg_isready -U appuser -d appdb
HealthInterval=10s

[Service]
Restart=always

[Install]
WantedBy=default.target

Activate:

systemctl --user daemon-reload
systemctl --user enable --now myapp-db
systemctl --user status myapp-db
journalctl --user -u myapp-db -f

Enable lingering so user services start at boot without login:

loginctl enable-linger $(whoami)

Legacy: podman generate systemd

For older Podman versions or quick one-off unit generation:

podman generate systemd --new --name myapp-api > ~/.config/systemd/user/myapp-api.service
systemctl --user daemon-reload
systemctl --user enable --now myapp-api.service

The --new flag makes the unit recreate the container on each start (stateless), which is the recommended pattern.

Comparison: Compose Orchestration Tools

ToolRootlessDaemonsystemd integrationCompose SpecKubernetes export
Docker ComposeNo (rootful)Yes (dockerd)External onlyFullNo
podman-composeYesNoVia Quadletv2/v3No
podman composeYesNoVia Quadletv2/v3No
Nerdctl + containerdPartial (rootless mode)Yes (containerd)LimitedFullNo
Podman QuadletYesNoNativeDeclarativeVia podman kube generate
Kubernetes (k3s/minikube)PartialYesExternalVia KomposeYes (native)

Podman Quadlet is the recommended path when you want tight systemd integration and automatic restarts. Use podman-compose when you need rapid iteration with an existing Compose-based workflow.

Migrating from Docker Compose

Step 1 — Alias docker to podman

alias docker=podman
alias docker-compose=podman-compose

Make permanent:

echo "alias docker=podman" >> ~/.bashrc
echo "alias docker-compose=podman-compose" >> ~/.bashrc

Step 2 — Update image references

Add the registry prefix docker.io/library/ to official images. Community images become docker.io/author/image:

# Before (Docker)
image: postgres:16-alpine

# After (Podman best practice)
image: docker.io/library/postgres:16-alpine

Step 3 — Add SELinux volume labels (RHEL/Fedora)

Append :Z to all bind mount volume paths on SELinux systems.

Step 4 — Remove privileged flags where possible

Podman’s rootless mode handles most use cases without privileged: true. Review each service and drop the flag where it is not strictly required.

Gotchas and Edge Cases

Networking differences: Rootless Podman uses pasta/slirp4netns instead of kernel bridge networking. Container-to-container communication within a pod works normally, but macvlan and ipvlan networks require root mode.

Port numbers below 1024: Rootless containers cannot bind host ports below 1024 by default. Set net.ipv4.ip_unprivileged_port_start=80 or use a reverse proxy container on port 8080.

docker.sock compatibility: Some tools (Portainer, Dockge) require the Docker socket. Podman provides a compatible socket via podman system service. Start it with systemctl --user start podman.socket and set DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock.

depends_on condition support: podman-compose supports condition: service_healthy from version 1.0.6+. Earlier versions ignore the condition and start immediately. Always use health checks to guard dependent services.

SELinux denials: If a volume mount fails on RHEL/Fedora, check ausearch -m avc and add the :Z label. Never disable SELinux — fix the label instead.

Troubleshooting

“short-name resolution” errors: Podman requires fully qualified image names. Add unqualified-search-registries = ["docker.io"] to /etc/containers/registries.conf or always prefix images with docker.io/.

“newuidmap/newgidmap not found”: Install uidmap package (sudo apt install uidmap or sudo dnf install shadow-utils).

Container cannot reach the internet: Check that slirp4netns or pasta is installed. On Ubuntu: sudo apt install slirp4netns. Verify with podman info | grep -A5 Network.

“Error: pasta failed” (Podman 5+): Install passt package. On Ubuntu 24.04: sudo apt install passt.

Volumes not persisting after podman-compose down: Named volumes are preserved. Data is lost only when you run podman-compose down -v or podman volume rm. Use podman volume ls to verify.

Summary

  • Podman is a daemonless, rootless-by-default container engine that is OCI-compatible and drop-in compatible with Docker CLI
  • podman-compose supports Compose Spec v2/v3 and runs multi-service stacks without root privileges
  • Configure /etc/subuid and /etc/subgid for rootless UID mapping before running multi-container workloads
  • Always use fully qualified image references (docker.io/library/postgres:16) and add :Z volume labels on SELinux systems
  • Quadlet provides native systemd integration for auto-start, restart, and journal logging of containerized services
  • For production workloads, prefer Quadlet over podman generate systemd for declarative, maintainable service definitions
  • Migrate from Docker Compose by aliasing docker=podman, updating image references, and adding SELinux volume labels