You cloned a repository that ships with a docker-compose.yml file
You run podman compose up -d expecting your web app, database, and cache to spin up together. The containers start, but the frontend cannot reach the backend. Or the database container crashes immediately with a permission denied error on the data volume. You are not doing anything wrong. Podman handles multi-container orchestration differently than Docker, and the default rootless configuration changes how networks and volumes behave.
What's actually happening
Docker runs a background daemon that owns all container resources. The daemon manages the storage driver, creates the network bridges, and holds the root credentials. Podman uses a fork-exec model with no daemon. Every container runs as a child process of your shell session. When you run podman compose, you are invoking a native Go plugin that translates the Compose specification into Podman commands. The plugin creates a network, starts containers, and links them. Because Fedora runs Podman rootless by default, every container gets its own user namespace. This means file ownership, network routing, and port publishing work differently than in a Docker environment.
Think of Docker like a managed hotel. The front desk handles all the keys, room assignments, and maintenance. Podman is like a co-op apartment building. You hold your own keys, you manage your own network wiring, and you are responsible for making sure shared spaces work correctly. The compose plugin is just a floor plan that tells you where to put the furniture.
The plugin architecture matters. Fedora ships the official podman-compose plugin via the podman-compose package. It is written in Go, not Python. It supports Compose specification version 3.9 and newer, and it integrates directly with Podman's rootless networking stack. The old Python wrapper that many tutorials reference is deprecated and will break on modern Fedora releases.
Run journalctl -xe first if a container fails to start. The plugin translates compose errors into journal entries when running as a rootless user. Read the actual error before guessing.
The fix and how to run it
Verify you are using the native plugin before you write any configuration.
podman compose version
# Confirms you are using the native Go plugin shipped by Fedora
# The output should show a version number and build date, not a Python traceback
# If it fails, run dnf install podman-compose to get the official package
Create a minimal docker-compose.yml in a test directory. Keep it simple to isolate networking and volume behavior.
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./html:/usr/share/nginx/html:Z
db:
image: postgres:15-alpine
environment:
POSTGRES_PASSWORD: testpass
volumes:
- pgdata:/var/lib/postgresql/data:Z
volumes:
pgdata:
The :Z suffix on the volume mounts is required for rootless Podman on Fedora. It tells SELinux to relabel the mounted directory so the container can read and write to it. Without it, you will get permission denied errors even if the Linux user permissions look correct. The :Z label marks the directory as private to that container. Use :z only if multiple containers need to share the same mount point.
Start the stack.
podman compose up -d
# -d runs the containers in detached mode so your terminal remains free
# The plugin reads the YAML, creates a network named after your project directory,
# and starts each service in dependency order
# It also generates a podman-compose.yaml state file to track the stack lifecycle
Check the running state.
podman compose ps
# Shows the status of each service defined in the compose file
# Use this instead of podman ps to see the logical service names rather than container hashes
# The output includes the container ID, status, and published ports
Stop the stack when you are done testing.
podman compose down
# Stops and removes all containers in the stack
# It also removes the default network created by the plugin
# Named volumes like pgdata are preserved unless you add the --volumes flag
Snapshot the system before the upgrade. Future-you will thank you.
Verify it worked
Confirm the services can communicate internally. The compose plugin automatically creates a user-defined network where service names resolve to container IPs.
podman compose exec web curl -s http://db:5432
# Tests internal DNS resolution between services
# The web container should receive a connection refused error from PostgreSQL,
# which proves the network route exists and DNS resolution works
# A timeout or network unreachable error means the CNI plugin failed to create the bridge
Check the logs for startup messages.
podman compose logs --tail=20
# Pulls recent logs from all services in the stack
# The --tail flag prevents flooding your terminal with historical output
# Use --follow to stream logs in real time while debugging
Inspect the network configuration if containers cannot reach each other.
podman network inspect $(podman compose config --services | head -1 | xargs podman inspect --format '{{.HostConfig.NetworkMode}}')
# Retrieves the CNI network details for the compose stack
# Look for the IPAM section to verify subnet allocation
# Rootless networks use a 10.89.x.x range by default on Fedora
Run systemctl status <unit> shows recent log lines AND state in one view. Always check status before restart. The same principle applies to compose stacks. Check podman compose ps and podman compose logs before tearing everything down.
Common pitfalls and what the error looks like
Volume permission errors are the most frequent blocker. If you omit the :Z or :z label, SELinux blocks the container from accessing the host directory. The container log will show:
standard_init_linux.go:228: exec user process caused: permission denied
This error appears even when the host user owns the directory. SELinux operates on labels, not just UID/GID. Always append :Z for private container data or :z for shared data. Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/. The same discipline applies to compose files. Keep your docker-compose.yml in your project directory, not in a system path.
Network isolation is another trap. By default, podman compose creates a user-defined network. Containers on this network can reach each other by service name, but they cannot reach the host network directly unless you publish ports. If your application tries to bind to 0.0.0.0 inside the container and expects host access, it will fail. Publish the port explicitly in the compose file.
Legacy script confusion causes silent failures. If you accidentally installed the old podman-compose Python package via pip, it will override the native plugin. The Python version does not support Compose specification version 3.9 or newer, and it lacks proper rootless networking support. Remove it immediately.
pip3 uninstall podman-compose
# Removes the deprecated Python wrapper that conflicts with the native plugin
# The native plugin is installed via dnf and requires no Python dependencies
# Verify removal by running which podman-compose and checking it points to /usr/libexec/podman/
Environment variable expansion fails when you mix host and container contexts. The compose plugin expands variables at parse time, not at runtime. If you reference a variable that does not exist in your shell environment, the plugin substitutes an empty string. Use a .env file in the same directory as the compose file to keep variables scoped.
podman compose config
# Validates the compose file and prints the fully resolved configuration
# Use this to catch variable expansion errors before starting containers
# It also shows the exact image tags and volume paths that will be used
Check the volume labels before blaming the application. SELinux denies silently until you look at the logs.
When to use this vs alternatives
Use podman compose when you are migrating an existing Docker workflow or collaborating with teams that already use Compose files. Use podman pod when you need tight networking between containers that share an IP address and loopback interface, which is common for monolithic applications split into microservices. Use systemd services when you are deploying a single long-running daemon that needs automatic restarts and boot-time initialization. Stay on the native podman run command when you only need a single container for testing or temporary tasks.
Trust the package manager. Manual file edits drift, snapshots stay.