You Don’t Actually Need a Daemon
Here’s the thing: Docker feels inevitable. It’s so easy to apt-get install docker.io, let it run as root, and forget about it until someone points out you’re six kernel versions behind and the privilege escalation CVE is already being traded on the dark web.
Podman flips the script. No daemon. No root. No sudo for every podman run you write. Just containers, orchestration, and the warm fuzzy feeling that your home lab isn’t a security dumpster fire waiting to catch someone’s attention.
And the kicker? You can run almost everything from Docker Compose YAML with barely a blink of effort. Drop-in. Minimal friction. The kind of thing that makes your 2 AM self actually grateful you’re awake.
The Rootless Revolution
Docker runs as root. Full stop. That daemon owns the socket, manages the network, touches the filesystem. One container goes sideways, and they’re potentially looking at your whole machine.
Podman inverted that: your user runs containers as your user. Rootless by default (though you can run it with sudo if you’re feeling nostalgic for bad decisions).
Here’s the mental model:
Docker: dockerd (root daemon) ← owns network, storage, process mgmt ├─ container process (runs in namespace) └─ container process (runs in namespace)
Podman: user process (unprivileged) ├─ container process (runs in user namespace) └─ container process (runs in user namespace)
(kernel handles the isolation, podman is just orchestrating)When you run podman run, Podman forks, sets up the Linux namespaces (PID, network, mount, UTS, ipc, user), and your container lives in that sandbox. Networking? Handled via slirp4netns or gvproxy on rootless. Port forwarding still works. Volumes still work. But it’s all happening in your user context, not as root.
This gets weird in places (we’ll get there), but the security model is cleaner. Full stop.
Podman Compose vs. Docker Compose: What Works
You’ve got two ways to do compose with Podman:
-
podman-compose— Standalone Python script. Reads Docker Compose YAML, translates to Podman commands. Works, but it’s a thin wrapper. Some things fall through the cracks. -
podman compose(Docker Compose plugin)** — Docker released their Compose as a Go binary plugin system. Podman ships with the same plugin interface. You installdocker-compose-pluginpackage, point it at Podman, and it “just works” like Docker Compose does for Docker.
# Install podman-compose (thin Python wrapper)sudo dnf install podman-compose # or apt, etc.podman-compose up -d
# Install Docker Compose plugin (better compatibility)sudo dnf install docker-compose-pluginpodman compose up -d # Uses the same binary as Docker, but talks to PodmanThe plugin approach is tighter. Fewer surprises. I’d lean toward that if your distro packages it.
Both read your docker-compose.yml and spin up containers. Both support networks, volumes, environment variables, healthchecks. The gaps are subtle and usually reveal themselves at 3 AM when you’re debugging why your service won’t start.
Where Podman Compose Gets Spicy
Network Aliases and DNS
Docker: You declare a service in docker-compose.yml, and the name becomes the DNS entry for other containers. postgres:5432 just works inside the network.
Podman rootless: Slirp4netns (the default networking backend) does DNS differently. Network aliases exist, but they sometimes resolve weirdly from one container to another. It’s not broken, but it’s fiddly.
The workaround: Use networks: and aliases: explicitly, or switch to gvproxy if your system supports it (Fedora ≥37, RHEL ≥9, etc.).
services: postgres: image: postgres:15 networks: - backend environment: POSTGRES_PASSWORD: example
app: image: myapp:latest depends_on: - postgres networks: - backend environment: DATABASE_URL: postgresql://postgres:5432/mydb
networks: backend: driver: bridgeThis version is explicit. Podman Compose handles it fine. When in doubt, be verbose.
Ports Below 1024
Docker (running as root): "80:8080" works. Bind to port 80, no problem.
Podman rootless: Port 80 requires CAP_NET_BIND_SERVICE. You don’t have it. So either:
- Run
podmanwithsudo(defeats the purpose) - Bind to a high port and use a reverse proxy (Caddy, nginx, whatever)
- Use systemd socket activation (fancier, but slick)
# Workaround: bind to 8080, let a proxy handle 80services: app: image: myapp:latest ports: - "8080:8080" # High port, rootless-friendlyThen run Caddy or nginx on the host (or in a separate rootful container, if you’re feeling weird) to proxy to 8080.
Healthchecks
Docker: Healthchecks work as documented. Container reports healthy/unhealthy. Compose respects depends_on: { service: condition: service_healthy }.
Podman Compose: Healthchecks exist, but some versions of podman-compose ignore them in depends_on conditions. Use the Docker Compose plugin if this matters to you.
services: db: image: postgres:15 healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s timeout: 5s retries: 5 environment: POSTGRES_PASSWORD: example
app: image: myapp:latest depends_on: db: condition: service_healthy # May not work with podman-composeVolumes and SELinux
Rootless Podman + SELinux = your volumes might be inaccessible inside the container. The UID/GID mapping (part of rootless) collides with SELinux context enforcement.
Workaround: Append :Z to the volume mount (tells Podman to relabel the volume for the container’s context) or :z (shared context across containers). Use :Z for most cases.
services: app: image: myapp:latest volumes: - ./data:/data:Z # Podman relabels for container accessThis is a Podman-ism. Docker doesn’t care because it’s running as root and SELinux can’t stop it. Podman has to play nice.
Secrets Handling
Docker Compose: Secrets are a first-class feature if you’re using Docker Swarm (old). Otherwise, just environment variables.
Podman Compose: No native secrets support in the old Python wrapper. Use environment files or bind mounts. The Docker Compose plugin does support the secrets: key, but they’re read from the host filesystem (not Swarm-synced).
Honestly, for home lab stuff, just use a .env file and don’t commit it:
# .env (gitignored)DB_PASSWORD=your_secret_hereAPI_KEY=another_secret
# docker-compose.ymlservices: app: env_file: .env environment: - DATABASE_PASSWORD=${DB_PASSWORD}A Real-World Podman Compose Stack
Here’s a working example: Caddy reverse proxy + a small Flask app + PostgreSQL. All rootless, all in Podman Compose.
version: '3.8'
services: postgres: image: postgres:15-alpine container_name: app-db environment: POSTGRES_USER: appuser POSTGRES_PASSWORD: securepassword POSTGRES_DB: appdb volumes: - db_data:/var/lib/postgresql/data:Z networks: - backend healthcheck: test: ["CMD", "pg_isready", "-U", "appuser"] interval: 10s timeout: 5s retries: 5
app: image: myapp:latest container_name: app-web depends_on: postgres: condition: service_healthy environment: DATABASE_URL: postgresql://appuser:securepassword@postgres:5432/appdb FLASK_ENV: production ports: - "8000:5000" volumes: - ./app:/app:Z networks: - backend restart: unless-stopped
caddy: image: caddy:latest container_name: app-proxy ports: - "8080:80" - "8443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data:Z - caddy_config:/config:Z networks: - backend restart: unless-stopped
volumes: db_data: caddy_data: caddy_config:
networks: backend: driver: bridgeAnd a Caddyfile:
example.local:8080 { reverse_proxy app:5000}To run it:
podman compose -f docker-compose.yml up -d# or with the plugin:podman compose up -d
# Check statuspodman compose ps
# View logspodman compose logs -f app
# Tear it downpodman compose down -v # -v removes volumesEverything works the same as Docker Compose. No daemon lurking. No root. Just containers doing container things.
Systemd Integration and Autostart
Here’s where Podman gets genuinely better than Docker.
Podman plays nice with systemd. You can generate systemd service files from your running containers or compose stacks:
# Run something, then export it as a servicepodman run -d --name my-service myimage:latestpodman generate systemd --name my-service > ~/.config/systemd/user/my-service.service
# Then:systemctl --user daemon-reloadsystemctl --user enable my-servicesystemctl --user start my-serviceFor compose stacks, the cleanest approach is to generate a unit per container after bringing the stack up, then enable each one:
podman-compose up -d
# Generate a unit for each container in the stackpodman generate systemd --name app-db > ~/.config/systemd/user/app-db.servicepodman generate systemd --name app-web > ~/.config/systemd/user/app-web.service
systemctl --user daemon-reloadsystemctl --user enable --now app-db app-webAlternatively, use Quadlets (Podman 4.4+) to define the whole stack as .container files — they’re systemd-native and survive reboots without any generate step. See the Quadlets section below.
Your containers now start at login, restart on crash, integrate with your systemctl commands. No cron jobs, no hacky init scripts. It’s clean.
This is a Podman advantage over Docker. Docker doesn’t integrate with systemd like this.
The Quadlets Option (Brief Mention)
If you want to go full systemd-native, Podman has Quadlets — a systemd-native way to define containers directly as .container files in ~/.config/containers/systemd/.
[Unit]Description=My AppAfter=network-online.target
[Container]Image=myapp:latestPublishPort=8080:5000Environment=DATABASE_URL=postgresql://localhost/mydbVolume=./data:/data:Z
[Install]WantedBy=default.targetThen systemctl --user enable myapp and it’s alive. No YAML. No compose file. Just a systemd unit file that Podman understands.
We’ve got a full article planned on this (it’s deeper than it sounds), so I’ll leave it at: Quadlets exist, they’re powerful, and they’re worth exploring if you’re already living in systemd land.
When to Stay on Docker
Podman isn’t a religion. Sometimes Docker is the right tool:
- Team consistency: Everyone knows Docker. Onboarding new people is easier.
- Enterprise support: Your org has Docker EE contracts and monitoring tools that don’t know Podman.
- Rootful containers: If you need root isolation (running untrusted workloads), Docker’s design is slightly less confusing.
- Swarm: You’re using Docker Swarm for orchestration. (Podman has no equivalent; use Kubernetes.)
- CI/CD pipelines: Your CI system was built for Docker socket access. Podman needs extra config.
- Weird image requirements: Some old images expect Docker-specific behavior. Rare, but it happens.
For home lab? Single-server stuff? Self-hosting your own services? Podman Compose is genuinely better. No daemon. Fewer permissions. Systemd integration. Drop-in compatibility that covers 95% of real-world use cases.
The Real Win
Podman Compose isn’t a hack. It’s not a workaround. It’s a legitimate alternative that trades the simplicity of “one daemon for everything” for the security of “everything runs as you.”
Your container orchestration becomes part of your user environment, not a trusted daemon running as root. That’s a meaningful shift in how you think about infrastructure security.
And the compose files? They’re identical. You’re not learning new syntax, rewriting your stacks, or fighting incompatibilities. Just slightly different wiring under the hood.
That’s a win worth the minor gotchas.