Networking¶
You publish -p 80:80 on a rootless container and it fails, with no obvious reason why — the same command
on port 8080 works fine. That gap, and the container that exits the instant you start it, are the two
support tickets that make new Podman users think something is broken. Neither is a bug; both trace back to
one fact: rootless has no daemon and no root, so networking has to be faked in userspace instead of
wired straight into the kernel.
The main course page shows the three network drivers by command; this page goes one layer deeper on why rootless port binding has limits, what actually forwards the traffic, how containers find each other by name, and the debug loop for that exited-container symptom.
TL;DR — in 30 seconds:
- Rootless networking is proxied through a user-mode helper (
pastaorslirp4netns), not routed straight into the kernel — that's what makes it work without root. - Publishing a host port below 1024 fails by default under rootless (it's still an unprivileged bind) — bump the port, lower the sysctl floor, or grant the capability.
- Name resolution between containers only works on a user-defined (named) network — the default network has no DNS resolver, so containers there must use IPs.
1. Why rootless networking needs help at all¶
A rootful container gets a real network namespace wired straight into the host's networking stack — the kernel routes traffic to it like any other interface. A rootless container's unprivileged user namespace has no such privilege: creating network interfaces, attaching to bridges, and binding privileged ports are all operations a normal Linux process can't do to another user's namespace.
Podman closes that gap with a user-mode network stack running as a helper process alongside the container. It presents the container with what looks like a normal network interface, but every packet in or out is proxied by the helper in userspace rather than routed by the kernel — slower than kernel routing, but it needs zero extra privilege.
flowchart LR
subgraph host["Host, unprivileged user"]
P["Published port<br/>e.g. 8080"]
H["User-mode network helper<br/>(pasta or slirp4netns)"]
end
subgraph ctr["Container's network namespace"]
C["Container process<br/>listening on e.g. 3000"]
end
P --> H --> C
2. pasta vs. slirp4netns¶
Both are user-mode network backends that do this proxying without root; they are not "rootless vs rootful," they are two implementations of the same rootless job.
slirp4netns |
pasta |
|
|---|---|---|
| Role | The original rootless network backend | Newer backend, default on current Podman |
| Performance | Slower (extra copy through a TAP device) | Faster (translates packets directly, fewer copies) |
| IPv6 | Limited | Full support |
| How to check | podman info \| grep -i network shows which backend is configured |
same |
You rarely choose between them explicitly — Podman picks the configured default — but knowing they exist matters for one reason: some networking oddities (unexpected latency, an IPv6 address not behaving) trace back to which backend is in play, not to your container or command being wrong.
3. Rootless port mapping and its limits¶
Publishing a port (-p hostport:containerport) asks the network helper to listen on the host side and
forward into the container. That listen step is still a normal, unprivileged bind on the host — and Linux
reserves ports below 1024 ("privileged ports") for root by default. An unprivileged user can bind
8080 without trouble; binding 80 fails, no matter how the container itself is configured.
flowchart TD
A["podman run -p 80:80<br/>rootless, unprivileged UID"] --> B{"Host port < 1024?"}
B -- "no, e.g. 8080" --> D["Bind succeeds — no help needed"]
B -- "yes" --> C["Blocked by default — pick a fix"]
C --> E["Bump the host port: -p 8080:80"]
C --> F["Lower the floor:<br/>sysctl net.ipv4.ip_unprivileged_port_start=80"]
C --> G["Grant the helper CAP_NET_BIND_SERVICE"]
All three fixes are legitimate; which one is right depends on whether you control the host's sysctl settings (a shared machine usually says no) and whether the service genuinely needs a low port at all — in practice, putting a reverse proxy on 8080 and letting a firewall or load balancer handle 80/443 sidesteps the whole question.
4. Container DNS — name resolution only exists on a named network¶
Podman runs a small internal DNS resolver for every user-defined (named) network you create with
podman network create. Containers attached to that same named network can resolve each other by
container name — postgres, api, whatever you passed to --name — with no extra configuration.
The default network Podman creates automatically (the one you're on if you never pass --network) does
not run that resolver. Two containers on it can still reach each other by IP, but a hostname lookup for
the other container's name fails — a common source of "my app can't find the database" that has nothing to
do with the database itself.
# No name resolution here — default network
podman run -d --name postgres docker.io/library/postgres:16-alpine
podman run --rm --network container:postgres docker.io/library/alpine getent hosts postgres # fails
# Name resolution works — named network
podman network create demo-net
podman run -d --name postgres --network demo-net docker.io/library/postgres:16-alpine
podman run --rm --network demo-net docker.io/library/alpine getent hosts postgres # resolves
The fix for "containers can't find each other by name" is almost always the same: create a named network and attach every container in the group to it, rather than debugging DNS itself.
5. Diagnostic: the container that exits immediately¶
The single most common "it's broken" report from a new Podman user is a container that's Exited a moment
after podman run -d — and it's a networking-adjacent symptom often enough (a port already bound, a
dependency the entrypoint can't reach) that it belongs here. One fixed loop finds the cause every time:
flowchart LR
A["podman logs <name><br/>what did the process print before it died?"] --> B["podman ps -a<br/>confirm Exited, read the exit code"]
B --> C["podman inspect <name><br/>full state: exit code, mounts, network, env"]
podman logs <name>first — most entrypoints print the actual error (a missing env var, a config file it couldn't read, a port already in use) to stdout/stderr before exiting. This is almost always enough on its own.podman ps -a— confirms the container is genuinelyExited(notCreatedand never started) and shows the numeric exit code in theSTATUScolumn.0means the process ran and returned cleanly (a one-shot command, or an entrypoint that isn't a long-running server); non-zero means it crashed.137specifically means it was killed (often an out-of-memory kill).podman inspect <name>last, only if the first two didn't explain it — the full JSON state, filtered with--format, e.g.podman inspect <name> --format '{{.State.ExitCode}}'or'{{.State.OOMKilled}}'to confirm a memory kill.
Reach for inspect last, not first — it's the most complete source but also the noisiest; logs answers
the question directly in the common case.
6. How this connects¶
- The user-mode network helper is the rootless counterpart to the same "no root needed" story as the user namespace remapping on the images & builds and storage pages — networking gets its own layer of that same trade-off (extra userspace hop, zero extra privilege).
- Named-network DNS here is the container-level version of the same idea Kubernetes Services provide at a much larger scale — a stable name that resolves without hardcoding an address.
- The
logs → ps -a → inspectloop generalizes past networking: it's the default first move for any "container isn't doing what I expected" report, not just an immediate exit.
Common gotchas
- Assuming
-p 80:80failing means the container is broken. Fix: it's the host-side unprivileged bind hitting the <1024 floor, not a container misconfiguration — bump the port, adjust the sysctl, or grantCAP_NET_BIND_SERVICE. - Expecting containers on the default network to resolve each other by name. Fix: the default
network runs no DNS resolver — create a named network with
podman network createand attach every container that needs to find the others by name. - Jumping straight to
podman inspectwhen a container exits immediately. Fix: checkpodman logs <name>first — most entrypoints print the actual failure (missing env var, port in use) before exiting;inspectis the noisiest source, not the first one. - Treating a
137exit code as a random crash. Fix:137specifically means the process was killed, often an out-of-memory kill —podman inspect --format '{{.State.OOMKilled}}'confirms it.
Check yourself — why does podman run -p 8080:80 work rootless while -p 80:80 doesn't?
The host-side listen for a published port is still an unprivileged bind, and Linux reserves ports
below 1024 for root by default. 8080 is above that floor and binds fine; 80 needs a fix — a higher
host port, a lowered sysctl floor, or the CAP_NET_BIND_SERVICE capability.
Check yourself — a container just exited right after podman run -d. What's the first command you run, and why not podman inspect first?
podman logs <name> first — most entrypoints print the actual error (missing env var, unreadable
config, port already in use) to stdout/stderr before dying, which usually answers the question
directly. inspect is the most complete source but also the noisiest, so it's the last resort, not
the first move.
You can defend this when you can explain why a rootless container can bind port 8080 but not 80, why
two containers on the default network can reach each other by IP but not by name, and what each step of
logs → ps -a → inspect is actually telling you.