The scenario
You cloned a project from GitHub. The repository includes a docker-compose.yml file. You run docker-compose up out of habit and get a command not found error. You switch to podman-compose up and the containers start, but one of them crashes immediately with a permission denied error on a mounted volume. You are trying to run a multi-container application on Fedora without installing the Docker daemon. The compose file works on your old setup, but Fedora handles container isolation differently. You need a reliable way to translate that YAML into working Podman commands without breaking your system.
What is actually happening
Docker Compose was built around a central background daemon. The daemon holds the network configuration, manages image layers, and keeps containers running after your terminal closes. Podman takes a different approach. It uses a fork-exec model. Every container runs as a direct child process of your shell or a systemd user service. There is no background daemon to manage state. When you close the terminal, the containers stop unless you explicitly detach them or hand them to systemd.
The podman-compose package bridges the syntax gap. It reads your docker-compose.yml file and translates the service definitions into direct podman commands. It does not run a daemon. It does not emulate Docker. It maps compose keys to podman flags. The translation is accurate for most standard workloads, but it will fail on Docker-specific extensions that rely on daemon-level features.
Think of it like a universal remote. The remote sends infrared signals that your TV understands. It does not contain the TV's operating system. It just translates button presses into commands the hardware can execute. podman-compose translates YAML into podman run, podman network create, and podman volume create. The tool runs in your user space by default. It respects Fedora's security boundaries. It does not require root privileges unless you explicitly request them.
Fedora's container stack uses the overlay storage driver and fuse-overlayfs for rootless containers. This means image layers are stored in your home directory under ~/.local/share/containers. The compose tool reads that path automatically. It does not need special configuration to find your images. It only needs the YAML file and a working Podman installation.
Install and run the stack
Fedora ships podman-compose in the official repositories. The package is a Python script that wraps the Podman CLI. Install it with dnf.
sudo dnf install podman-compose
# WHY: pulls the Python wrapper and its dependencies from the Fedora repos
# WHY: installs the podman-compose binary to /usr/bin
# WHY: no daemon is started during installation
Navigate to the directory containing your compose file. Run the stack in detached mode so the terminal remains free.
cd ~/my-project
# WHY: moves into the project root where docker-compose.yml lives
podman-compose up -d
# WHY: reads the YAML file and creates networks, volumes, and containers
# WHY: the -d flag detaches the process so your shell stays responsive
# WHY: podman-compose translates service definitions into podman run commands
If the project uses a custom compose file name, pass it explicitly with the -f flag. The tool expects docker-compose.yml or compose.yml by default.
podman-compose -f production-compose.yml up -d
# WHY: overrides the default filename search
# WHY: useful when you maintain separate files for development and staging
# WHY: the flag must come before the up subcommand
Stop the stack when you are done testing. The down command removes containers, networks, and anonymous volumes created by the compose file.
podman-compose down
# WHY: stops all running services defined in the YAML
# WHY: removes the isolated network created for the stack
# WHY: cleans up anonymous volumes but preserves named volumes
Stream the logs to verify the application started cleanly. Use the -f flag to follow new output.
podman-compose logs -f
# WHY: attaches to the stdout and stderr of all services in the stack
# WHY: the -f flag keeps the stream open for real-time debugging
# WHY: press Ctrl+C to detach without stopping the containers
Reboot before you debug. Half the time the symptom is gone.
Verify it worked
Check the container state immediately after the command finishes. Podman tracks running containers in the local user namespace or root namespace depending on how you invoked the command.
podman ps
# WHY: lists all running containers managed by your current user
# WHY: shows container ID, image name, status, and mapped ports
# WHY: confirms the fork-exec processes are actually alive
Verify the network isolation. Compose creates a dedicated bridge network for your services. Containers on that network can reach each other by service name.
podman network ls
# WHY: displays all podman networks including the compose-generated one
# WHY: confirms the network name matches the project directory name
# WHY: helps you identify leftover networks from previous runs
Check port bindings to ensure external traffic can reach your services.
ss -tlnp | grep podman
# WHY: lists active TCP listeners and their associated processes
# WHY: verifies that podman is actually binding to the expected ports
# WHY: run with sudo if the process belongs to root
Run journalctl -xe first. Read the actual error before guessing.
Compose file compatibility and translation limits
The translation layer handles most standard keys. image, ports, environment, volumes, and depends_on map directly to podman equivalents. You will run into friction when the YAML uses Docker-specific features.
Docker uses a daemon-level secret store. Podman does not. If your compose file references secrets, the tool will warn you and skip the injection. Use environment variables or mounted files instead.
Docker Swarm mode uses deploy keys for replicas and resource limits. podman-compose ignores deploy entirely. Replace it with scale in the CLI or use systemd to manage multiple instances.
Docker build contexts sometimes rely on multi-stage builds that cache layers in the daemon. Podman builds run locally and cache in ~/.local/share/containers/storage. The build will succeed, but you may see longer initial build times as the overlay filesystem initializes.
Network aliases and custom DNS settings translate correctly. Podman creates a local DNS resolver for each compose network. Service discovery works out of the box. You do not need to edit /etc/hosts or configure external DNS servers.
Volume drivers like local work as expected. Custom volume plugins do not. Stick to bind mounts and named volumes. Named volumes are stored under ~/.local/share/containers/storage/volumes. They survive container removal and can be shared across stacks.
Common pitfalls and what the error looks like
Volume mounts are the most frequent failure point. Fedora enforces SELinux by default. Podman runs rootless by default. When you mount a host directory into a rootless container, the container process runs under a different user ID and security context. SELinux blocks the access unless you relabel the mount.
The compose file will mount ./data:/app/data. The container crashes with a permission error. You will see this in the logs:
standard_init_linux.go:228: exec user process caused: permission denied
Fix it by adding the :Z or :z suffix to the volume definition in your YAML file. The :Z flag relabels the content with a private label for this container only. The :z flag relabels it as shared between multiple containers.
volumes:
- ./data:/app/data:Z
# WHY: tells SELinux to relabel the host directory for this container
# WHY: prevents permission denied errors on rootless mounts
# WHY: use :z only if multiple containers need read-write access to the same path
If you cannot edit the compose file, relabel the directory manually before starting the stack.
chcon -Rt svirt_sandbox_file_t ./data
# WHY: applies the SELinux type expected by container runtimes
# WHY: -R applies the change recursively to all files and subdirectories
# WHY: this is a temporary workaround that resets on a full system relabel
Port conflicts also break the stack. Podman does not override existing listeners. If a service is already bound to port 8080, the new container fails to start. The error appears in the compose output:
Error: failed to bind port 0.0.0.0:8080: address already in use
Check what is listening on the port before starting the stack.
ss -tlnp | grep 8080
# WHY: lists active TCP listeners and their associated processes
# WHY: identifies whether another container or system service holds the port
# WHY: run with sudo if the process belongs to root
Rootless Podman also has limits on shared memory and network namespaces. If your application uses heavy IPC or requires raw network access, it will fail under the default rootless configuration. Run podman info to check your namespace limits. Increase the shared memory size in your compose file if needed.
services:
app:
shm_size: "1gb"
# WHY: overrides the default 64mb shared memory limit for rootless containers
# WHY: required for applications that use heavy inter-process communication
# WHY: podman-compose passes this directly to podman run --shm-size
SELinux denials show up in journalctl -t setroubleshoot with a one-line summary. Read those before disabling SELinux. Disabling SELinux breaks the security model and exposes your host to container escapes. Fix the context instead.
Trust the package manager. Manual file edits drift, snapshots stay.
When to use this vs alternatives
Use podman-compose when you need to run existing Docker Compose files on Fedora without installing the Docker daemon. Use native podman commands when you are writing new infrastructure and want direct control over networks and volumes. Use systemd unit files when you need containers to survive reboots and restart automatically on failure. Use docker from the official repositories when you are locked into Docker-specific features like Swarm or proprietary plugins. Stay on podman-compose if your workflow relies on YAML-based orchestration and you want to keep the system daemon-free.