The scenario
You copied a docker-compose.yml file from a project repository, ran podman-compose up, and the terminal immediately printed a permission denied error on a volume mount. Or maybe the container starts but refuses to bind to port 80, and your browser shows a connection refused message. You are on Fedora. Docker is not installed. You want the same multi-container workflow without a background daemon.
What is actually happening
Docker Compose relies on a central Docker daemon that talks to the kernel via a Unix socket. The daemon owns the containers, manages the networks, and handles volume mounts with root privileges. Podman works differently. It runs containers as regular processes under your user account. There is no daemon. When you run a compose file, a translation layer reads the YAML, maps Docker-specific directives to Podman equivalents, and spawns the containers directly.
The podman-compose Python script handles the heavy lifting for older or complex files. It parses the YAML, generates the equivalent podman run and podman network create commands, and executes them sequentially. The newer podman compose built-in plugin does the same translation in Go, but the Python wrapper still catches edge cases that the plugin misses. Rootless execution means everything runs under your UID. Network namespaces are isolated. Port forwarding uses slirp4netns or pasta instead of host-level iptables. SELinux watches every file access and blocks containers from touching host directories unless you explicitly allow it.
Run podman info first. Check the rootless and slirp4netns fields before you assume the network stack is broken.
The fix or how-to
Install the tools. Fedora ships Podman by default on Workstation, but the compose translation layer lives in a separate package. The Python wrapper remains the most reliable option for complex legacy files.
Here is how to install the compose wrapper alongside the core engine.
sudo dnf install podman podman-compose
# dnf resolves dependencies automatically.
# podman-compose pulls in python3-pyyaml and python3-podman.
# The package places the wrapper in /usr/bin/podman-compose.
Navigate to your project directory. The YAML file must be named docker-compose.yml or compose.yml. Podman compose tools look for those exact names by default.
Here is how to start the stack in detached mode.
podman-compose up -d
# -d runs containers in the background.
# The wrapper reads the YAML and creates a pod or network.
# It maps service names to container names automatically.
If you need to tear everything down, use the down command. It stops the containers, removes the networks, and cleans up anonymous volumes.
podman-compose down
# Stops all services defined in the file.
# Removes the podman-compose network namespace.
# Leaves named volumes intact unless you add -v.
Volume mounts are where most Fedora users hit a wall. SELinux prevents containers from reading host directories by default. You have two options. Add the :Z suffix to the volume mapping in your YAML file, or relabel the directory on the host. The :Z suffix tells Podman to relabel the mount point automatically. It works for single-user setups. It isolates the directory from other users.
Here is how to relabel a host directory if you prefer manual control.
sudo chcon -R -t svirt_sandbox_file_t /path/to/host/data
# chcon changes the SELinux context of existing files.
# svirt_sandbox_file_t allows containers to read and write.
# -R applies the change recursively to subdirectories.
Rootless containers cannot bind to privileged ports below 1024. They also cannot use host networking by default. If your compose file maps port 80 or 443, change it to 8080 or 8443. If you need to expose a port to external machines, firewalld blocks the traffic until you add a rule. Podman rootless containers use a user-specific subnet. The firewall must allow inbound traffic to the host port.
Here is how to open the port and apply the rule.
sudo firewall-cmd --add-port=8080/tcp --permanent
# Adds the port to the persistent firewall configuration.
# --permanent ensures the rule survives a reboot.
sudo firewall-cmd --reload
# Reloads the runtime configuration to match persistent rules.
# Always run reload after editing persistent rules.
The built-in podman compose plugin is available on Fedora 40 and newer. It drops the hyphen and runs faster. It shares the same YAML parser as the core CLI. Use it when your compose file follows modern standards. Keep the Python wrapper when you need legacy compatibility.
Snapshot your compose directory before modifying volume paths. Future-you will thank you when a bad relabel breaks a production script.
Verify it worked
Check the container state. Podman lists running containers with ps. The output shows the container ID, image, command, creation time, status, and port mappings.
Here is how to confirm the stack is running and bound to the correct ports.
podman ps
# Lists all running containers for your user.
# Shows the mapped ports in the PORTS column.
# Use -a to see stopped or exited containers.
Check the logs. Compose tools route stdout and stderr to the container runtime. Podman stores them in JSON files under ~/.local/share/containers/storage/overlay-containers/. The logs command reads them directly.
Here is how to tail the logs for a specific service.
podman logs <container_name_or_id>
# Streams the stdout and stderr of the container.
# Add -f to follow new log lines in real time.
# Use --tail 50 to limit output to recent entries.
Test the network binding. Use curl or ss to verify the port is listening. Rootless containers bind to 127.0.0.1 or 0.0.0.0 depending on your compose file. The ss command shows the actual socket state.
Here is how to verify the port is open and accepting connections.
ss -tlnp | grep 8080
# Shows listening TCP sockets on port 8080.
# -t filters for TCP, -l for listening, -n for numeric ports.
# -p prints the process name and PID.
Run journalctl -xeu podman if a container crashes immediately. The journal shows kernel messages, SELinux denials, and cgroup limits in one view. Read the actual error before guessing.
Common pitfalls and what the error looks like
The most frequent failure is a SELinux denial on volume mounts. The container starts, but the application inside cannot read its configuration files. The terminal prints Permission denied or open /app/config.yml: permission denied. The journal shows a one-line summary from setroubleshoot.
type=AVC msg=audit(1715423891.123:45): avc: denied { read } for pid=1234 comm="node" name="config.yml" dev="sda1" ino=567890 scontext=system_u:system_r:container_t:s0:c123,c456 tcontext=unconfined_u:object_r:home_root_t:s0 tclass=file permissive=0
The tcontext shows home_root_t. The container expects svirt_sandbox_file_t. Add :Z to the volume mapping or run chcon on the host directory. Never disable SELinux to fix this. Relabel the context instead.
Another common issue is port binding failure in rootless mode. The compose file maps 80:80. Podman refuses to bind to port 80 without root privileges. The terminal prints Error: cannot expose privileged port 80: you need to add 'CAP_NET_BIND_SERVICE' to the container. Change the host port to something above 1024. Use 8080:80 in the YAML.
Legacy compose files sometimes use privileged: true. Rootless Podman cannot grant full kernel privileges. The wrapper prints Error: privileged mode is not supported in rootless mode. Remove the flag. Add specific capabilities instead. Use cap_add: - NET_ADMIN or - SYS_PTRACE depending on what the container actually needs.
Compose version mismatches cause silent failures. Docker Compose v2 syntax uses services: at the top level. Older v1 files use image: and command: at the root. The Python wrapper tries to auto-detect the version. It fails when the YAML structure is ambiguous. Add version: "3.8" or version: "2.4" to the top of the file. The wrapper parses it correctly.
Trust the package manager. Manual file edits drift, snapshots stay.
When to use this vs alternatives
Use podman-compose when you are migrating a legacy Docker stack that relies on complex volume drivers or non-standard network aliases. Use podman compose when you are writing new services and want faster startup times with native Go integration. Use Docker Compose when you are working in a corporate environment that mandates a centralized container registry and daemon-based orchestration. Use Podman Quadlet when you need systemd to manage container lifecycles and automatic restarts. Stay on the upstream Podman CLI if you only run single containers and do not need multi-service coordination.
Read the compose file before you run it. One bad volume path can fill your disk with anonymous layers.