Rootless containers

Rootless containers run as your user without sudo, using crun and passt for secure isolation.

Rootless containers

You followed a tutorial that told you to run sudo docker run. You switched to Fedora, installed podman, and tried the same command without sudo. The container started, but the network dropped. Or maybe you got a permission denied error when trying to bind to port 80. You want to run containers without handing over root privileges to every script you pull from the internet. You also want the system to stay clean when you delete the container.

What is actually happening

Rootless containers run inside your user namespace instead of the host root namespace. The container process sees itself as root inside the container, but the kernel maps that identity to your regular user ID on the host. Fedora uses crun as the OCI runtime and passt for networking. crun handles the low-level namespace setup and cgroup isolation. passt manages the virtual network bridge and NAT without requiring iptables or nftables privileges.

Think of it like a hotel room. You have a master key to your room, but you cannot open the doors to other guests or the building's electrical panel. The container gets the same treatment. It controls its own filesystem and processes, but it cannot touch the host kernel or other users. The kernel enforces the boundary through user namespace mapping. When the container writes to a file as UID 0, the kernel translates that to your host UID before touching the disk. When the container tries to load a kernel module, the call fails because the user namespace lacks the CAP_SYS_MODULE capability.

This architecture removes the need for a background daemon. Every podman command spawns a short-lived process that sets up the namespace, runs the container, and exits. You can run podman inside another container without nesting daemons. You also avoid the security risk of a privileged socket listening on the host.

The fix or how-to

Fedora Workstation ships with podman preinstalled. You still need to verify the supporting tools are present. Run the installation command to ensure crun and passt are available in your environment.

sudo dnf install podman crun passt
# dnf resolves dependencies and places binaries in /usr/bin
# crun provides the OCI runtime that handles namespace creation
# passt replaces slirp4netns for faster, more reliable rootless networking

Next, check your user namespace limits. The kernel restricts how many UIDs and GIDs a single user can map. If the limit is too low, container creation fails with a namespace error. Fedora usually configures this correctly, but dual-boot systems or migrated home directories sometimes carry stale limits.

cat /etc/subuid
# shows the range of host UIDs your user can map into containers
cat /etc/subgid
# shows the range of host GIDs your user can map into containers

If the ranges are missing or too small, you need to adjust them. The sysctl settings control the maximum range size. Most desktop setups require at least 65536 entries.

sysctl user.max_user_namespaces
# verifies the kernel allows enough namespace allocations
sudo sysctl -w user.max_user_namespaces=15000
# raises the limit if the default is too restrictive for your workload

Now run a test container. The command below pulls an image, starts an interactive shell, and removes the container automatically when you exit.

podman run --rm -it docker.io/library/alpine:latest /bin/sh
# --rm deletes the container filesystem immediately after exit
# -it allocates a pseudo-TTY and keeps stdin open for interactive use
# alpine:latest pulls a minimal image that boots in under a second
# /bin/sh starts a shell so you can verify the environment

Convention aside: podman does not run a background daemon. Every podman command spawns a short-lived process that sets up the namespace, runs the container, and exits. This means you can run podman inside another container without nesting daemons.

Run the container from your normal user account. Do not prefix it with sudo. The runtime will fail if you try to force root privileges on a rootless setup.

Verify it worked

Check the process tree and namespace mapping. Use ps and lsns to confirm the container runs under your UID and lives in an isolated user namespace.

ps aux | grep alpine
# lists the running container process and its parent podman-run
lsns -t user
# displays active user namespaces and their associated UIDs

Verify the network stack. passt creates a virtual interface inside the container. Check the routing table and default gateway to ensure traffic leaves the container correctly.

ip route
# shows the default route pointing to the passt virtual gateway
ip addr
# lists the eth0 interface with a 10.0.2.x address range
ping -c 3 1.1.1.1
# confirms outbound DNS and routing work without host firewall interference

Run lsns -t user before you assume the container is broken. If the namespace isn't showing up, the runtime failed to isolate the process.

Common pitfalls and what the error looks like

Port binding below 1024 requires root privileges on the host. Rootless containers cannot bind to privileged ports unless you adjust the net.ipv4.ip_unprivileged_port_start sysctl. You will see a permission denied error when the container tries to listen on port 80 or 443.

Error: failed to start container "web": failed to create container: 
permission denied: failed to bind to port 80: operation not permitted

Fix the sysctl setting to allow unprivileged port binding. The change applies immediately to the running kernel.

sudo sysctl -w net.ipv4.ip_unprivileged_port_start=1
# lowers the threshold so rootless containers can bind to standard HTTP ports
sudo sysctl -p /etc/sysctl.d/99-unprivileged-ports.conf
# persists the setting across reboots

Storage driver conflicts also cause silent failures. Fedora defaults to overlay with fuse-overlayfs for rootless setups. If you previously used docker with vfs or devicemapper, the storage directory gets corrupted. Clear the old storage and let podman regenerate it.

podman system reset -f
# removes all containers, images, and volumes to clear driver conflicts
podman info | grep graphDriverName
# confirms the runtime switched back to fuse-overlayfs

Convention aside: Config files in /etc/containers/ control podman behavior. Files in /usr/share/containers/ ship with the package. Edit /etc/containers/storage.conf to change the graph root or driver. Never edit the upstream defaults.

Networking fallbacks sometimes trigger when passt is missing from the PATH. The runtime will silently fall back to slirp4netns, which has higher CPU overhead and stricter firewall rules. You will notice slower package downloads and dropped DNS queries.

level=warning msg="fallback to slirp4netns: passt not found in PATH"

Install passt explicitly and restart your shell to refresh the environment variables. Run journalctl -xeu podman if you need to trace the exact fallback trigger.

Trust the package manager. Manual file edits drift, snapshots stay.

When to use this vs alternatives

Use rootless containers when you are running development tools, CI agents, or personal services on a shared desktop. Use rootful containers when you need to bind to privileged ports, manage low-level network namespaces, or run infrastructure services that require host device access. Use systemd user services when you want the container to survive a logout and restart automatically. Use a virtual machine when you need a completely isolated kernel, hardware passthrough, or nested virtualization. Stay on the default rootless setup if you only need standard application isolation and package management.

Where to go next