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 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
sudoaccess 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:
| Aspect | Docker | Podman |
|---|---|---|
| Architecture | Client-server daemon | Fork/exec, daemonless |
| Default privilege | Runs as root | Rootless by default |
| Compose support | docker compose plugin | podman-compose or podman compose |
| Image format | OCI (Docker-compatible) | OCI (Docker-compatible) |
| Socket requirement | /var/run/docker.sock (root) | Optional user socket |
| systemd integration | External unit files | Native Quadlet support |
| CLI compatibility | Reference implementation | Drop-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.:Zvolume label: On SELinux-enabled systems (Fedora, RHEL, Rocky), the:Zsuffix 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
| Tool | Rootless | Daemon | systemd integration | Compose Spec | Kubernetes export |
|---|---|---|---|---|---|
| Docker Compose | No (rootful) | Yes (dockerd) | External only | Full | No |
| podman-compose | Yes | No | Via Quadlet | v2/v3 | No |
podman compose | Yes | No | Via Quadlet | v2/v3 | No |
| Nerdctl + containerd | Partial (rootless mode) | Yes (containerd) | Limited | Full | No |
| Podman Quadlet | Yes | No | Native | Declarative | Via podman kube generate |
| Kubernetes (k3s/minikube) | Partial | Yes | External | Via Kompose | Yes (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/subuidand/etc/subgidfor rootless UID mapping before running multi-container workloads - Always use fully qualified image references (
docker.io/library/postgres:16) and add:Zvolume 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 systemdfor declarative, maintainable service definitions - Migrate from Docker Compose by aliasing
docker=podman, updating image references, and adding SELinux volume labels