| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64netavark-1.17.2-1.el10.x86_64 |
| Applies to | Any Linux host with Podman installed |
| Privilege | Rootful examples as root; rootless pasta example as an unprivileged user |
| Scope | Publishing container ports with -p, -P, and podman port — host binding, random ports, TCP/UDP, port ranges, pod-level publishing, network-mode limits, and rootless forwarding behavior. Does not cover privileged ports below 1024, full port-mapping troubleshooting, firewalld, or Netavark internals. |
| Related guides | Podman networking modes Run containers with podman run |
A service inside a container listens on a port in its own network namespace. To reach it from the host or LAN, you publish a host port that Podman forwards into the container. The mapping looks like this:
host:8080
↓
Podman port forwarding
↓
container:80This guide covers every practical -p form, how to read mappings with podman port, and where publishing stops working — pods, host networking, macvlan, and rootless bridge forwarding.
The lab image is docker.io/library/nginx:alpine, which listens on container port 80.
Podman port mapping syntax
The essential publish form maps one host port to one container port:
-p HOST_PORT:CONTAINER_PORTRun nginx with host port 8080 forwarded to container port 80:
podman run -d --name podman-port-demo -p 8080:80 docker.io/library/nginx:alpineAsk Podman which host port is bound:
podman port podman-port-demoSample output:
80/tcp -> 0.0.0.0:8080Read that line as “container TCP port 80 is reachable on all host IPv4 addresses at port 8080.” Confirm the service answers:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8080/Sample output:
HTTP 200-p and --publish are equivalent. Repeat -p for multiple mappings on one container.
Bind to all host interfaces
When you omit the host IP, Podman binds the published port on every host address — conceptually 0.0.0.0:8080 for IPv4:
podman run -d --name port-all-if -p 8080:80 docker.io/library/nginx:alpineConfirm the mapping lists 0.0.0.0:
podman port port-all-ifSample output:
80/tcp -> 0.0.0.0:8080On the host, the listener covers all interfaces:
ss -ltnp | grep ':8080'Sample output:
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("conmon",pid=182323,fd=5))Binding everywhere is convenient for LAN access but not always desirable. Use a specific host IP or 127.0.0.1 when you want to limit who can connect.
Bind a container port to 127.0.0.1
Prefix the mapping with the loopback address so only local clients on the host can connect:
podman run -d --name port-loop -p 127.0.0.1:8081:80 docker.io/library/nginx:alpineThe publish map should show loopback only:
podman port port-loopSample output:
80/tcp -> 127.0.0.1:8081ss confirms the narrower bind:
ss -ltnp | grep ':8081'Sample output:
LISTEN 0 4096 127.0.0.1:8081 0.0.0.0:* users:(("conmon",pid=182402,fd=5))Remote hosts cannot reach 8081 unless they tunnel through the host. Local development and sidecar patterns often use this shape.
Bind to a specific host IP
Publish on one host address when the machine has several interfaces. Use an IP that actually exists on the host — on the lab VM enp0s3 carries 10.0.2.15:
podman run -d --name port-hostip -p 10.0.2.15:8082:80 docker.io/library/nginx:alpineVerify Podman bound only that address:
podman port port-hostipSample output:
80/tcp -> 10.0.2.15:8082Traffic to 10.0.2.15:8082 reaches the container; other host addresses do not publish that port. Use an address configured on the host. On a normal host, binding an address that is not locally assigned fails unless the system has been explicitly configured for non-local binds.
Let Podman choose the host port
Omit the host port number and Podman picks a free ephemeral port:
podman run -d --name port-random -p 80 docker.io/library/nginx:alpineDiscover which host port Podman picked:
podman port port-randomSample output:
80/tcp -> 0.0.0.0:44003Query one container port directly:
podman port port-random 80Sample output:
0.0.0.0:44003You can also leave the host side empty while pinning the bind address:
podman run -d --name port-random2 -p 127.0.0.1::80 docker.io/library/nginx:alpineQuery the assigned loopback port for container port 80:
podman port port-random2 80Sample output:
127.0.0.1:41295Random host ports suit tests, CI jobs, and temporary services where you only need podman port to discover the assignment.
Publish TCP and UDP ports
TCP is the default when you omit a protocol suffix. For UDP, append /udp:
podman run -d --name port-udp -p 15353:53/udp docker.io/library/alpine:3.20 sleep 600The mapping line should name UDP explicitly:
podman port port-udpSample output:
53/udp -> 0.0.0.0:15353Publish both protocols on the same host port when the service needs it:
podman run -d --name port-both -p 15354:53/tcp -p 15354:53/udp docker.io/library/alpine:3.20 sleep 600Both protocol lines should share host port 15354:
podman port port-bothSample output:
53/tcp -> 0.0.0.0:15354
53/udp -> 0.0.0.0:15354Rootful Podman can publish SCTP where the host stack supports it (/sctp). Most web and API workloads only need TCP.
Publish a range of ports
Map matching host and container ranges when cardinality must align:
podman run -d --name port-range -p 8000-8002:8000-8002/tcp docker.io/library/alpine:3.20 sh -c 'apk add --no-cache busybox-extras >/dev/null 2>&1; for p in 8000 8001 8002; do busybox nc -lk -p $p -e echo ok-$p & done; sleep 600'After the listeners start, inspect every mapping:
podman port port-rangeSample output:
8000/tcp -> 0.0.0.0:8000
8001/tcp -> 0.0.0.0:8001
8002/tcp -> 0.0.0.0:8002Three host ports map to three container ports one-to-one. A mismatch in range length is rejected at create time.
Publish all exposed ports with -P
-P (or --publish-all) assigns random host ports to every port declared in the image metadata:
podman run -d --name port-pall -P docker.io/library/nginx:alpine-P assigns a random host port for each exposed image port:
podman port port-pallSample output:
80/tcp -> 0.0.0.0:34467Compare that to the image metadata, which only documents the container port:
podman inspect port-pall --format '{{json .Config.ExposedPorts}}'Sample output:
{"80/tcp":{}}Important distinction:
EXPOSE in Containerfile → image metadata only
-p / -P → actual host publicationEXPOSE documents intent; it does not open a host listener. Only -p, -P, or an orchestrator equivalent does.
Inspect port mappings with podman port
podman port is the authoritative check — do not guess which ephemeral port Podman chose.
All mappings for one container:
podman port podman-port-demoOne container port:
podman port podman-port-demo 80With explicit protocol:
podman port podman-port-demo 80/tcpEvery running container with published ports:
podman port --allSample output (trimmed):
725f8829cb80
80/tcp -> 0.0.0.0:8080
080cc917ba8f
80/tcp -> 127.0.0.1:8081
7e0073b18f59
80/tcp -> 10.0.2.15:8082podman port --all lists each running container ID followed by its known port mappings.
Port publishing depends on network mode
Port publishing requires a network mode where Podman can forward host traffic into the container. Podman supports -p with bridge networking and rootless pasta; other modes such as host, none, macvlan, and ipvlan do not use Podman's normal port-publishing path.
| Network mode | -p supported |
|---|---|
| bridge (default rootful) | Yes |
| pasta (default rootless) | Yes |
| host | Not needed — container shares host stack |
| none | No external path to publish |
| macvlan | No Podman port forwarding |
| ipvlan | No Podman port forwarding |
With --network host, services listen on host addresses directly. podman port returns nothing because there is no publish map:
podman run -d --name port-host --network host docker.io/library/nginx:alpineWith host networking there is nothing to list:
podman port port-hostThe command exits successfully with no output. Macvlan and ipvlan attach containers to a parent NIC — see Create and manage Podman networks for those drivers; use their LAN addresses instead of -p.
Port mapping in Podman pods
Containers in a pod share one network namespace. Publish ports on the pod, not on member containers:
podman pod create --name web-pod -p 8090:80Add nginx as a pod member — no extra -p on the container:
podman run -d --pod web-pod --name web-pod-c1 docker.io/library/nginx:alpineMember containers inherit the pod publish map:
podman port web-pod-c1Sample output:
80/tcp -> 0.0.0.0:8090podman port web-pod fails because the pod is not a container — query a member instead. The service is reachable:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8090/Sample output:
HTTP 200Trying to add -p on a container that joins an existing pod is rejected:
podman pod create --name bad-podNow try publishing on a container inside that pod:
podman run -d --pod bad-pod -p 8091:80 --name bad-pod-c1 docker.io/library/nginx:alpineSample output:
Error: invalid config provided: published or exposed ports must be defined when the pod is created: network cannot be configured when it is shared with a podPod topology and shared interfaces are covered in Podman pod networking.
Rootless Podman port forwarding
Rootless behavior splits along the network mode Podman picks.
Default rootless pasta networking
Since Podman 5.0, rootless containers default to pasta. Port publishing syntax is the same:
podman run -d --name port-rootless -p 8083:80 docker.io/library/nginx:alpineRootless pasta uses the same podman port output format:
podman port port-rootlessSample output:
80/tcp -> 0.0.0.0:8083Pasta forwarding preserves the original client source IP in application logs — useful when you need the real remote address behind -p.
Rootless user-defined bridge network
A rootless container on a custom Netavark bridge uses rootlessport for -p forwarding on Podman 5.x. That path works for connectivity but does not preserve the client source IP. Logs often show an address from the rootless bridge or forwarder instead of the remote client.
If you need client IP preservation on a user-defined bridge, read Rootless Podman networking for the trade-offs between pasta and bridge modes.
Preserve client IP on rootless bridge networks
rootlessport and the option below is not the tested procedure. Newer Podman/containers-common releases support rootless_port_forwarder="pasta" when the installed passt provides Pesto support.
On supported releases you can switch the rootless bridge forwarder in containers.conf:
[network]
rootless_port_forwarder="pasta"That selects pasta's Pesto forwarding for bridge networks so client source IPs are preserved. The feature is experimental, requires pesto support in passt, and may change between releases. Stay on default pasta networking on Podman 5.x if client IP preservation matters and you do not need a custom bridge.
Rootless ports below 1024
Unprivileged users cannot bind host ports below 1024 by default. A rootless -p 80:80 or -p 443:443 often fails with a permission error.
Fixes — sysctl tuning, firewalld redirects, reverse proxies — belong in Rootless Podman privileged ports. Publish on 8080 during development or run rootful when you truly need host port 80.
podman port vs EXPOSE
| Feature | Purpose |
|---|---|
Containerfile EXPOSE |
Documents expected container ports in image metadata |
podman run -p |
Maps a chosen host port (and optional host IP) to a container port |
podman run -P |
Publishes every exposed image port to a random host port |
podman port |
Shows the resulting host bindings for a running container |
Build tools and compose files may read EXPOSE; only -p / -P (or equivalent orchestration) creates host listeners.
Common port mapping mistakes
| Mistake | What goes wrong | Where to read more |
|---|---|---|
App listens on 127.0.0.1 inside the container |
Published port exists but connections reset or refuse | Fix Podman port mapping not working |
Wrong container port in -p |
Traffic reaches the wrong socket or nothing listens | Verify with ss -lntup inside the container |
-p on a pod member |
Create rejected or ignored | Publish on podman pod create |
--network host with -p |
No publish map; confusing podman port output |
Use host networking without -p |
| Host firewall blocks the port | Local curl works; remote clients time out |
Firewall troubleshooting (out of scope here) |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
podman port shows mapping but connection refused |
Service bound to 127.0.0.1 inside container |
Reconfigure app to listen on 0.0.0.0 or container IP |
invalid config provided with pod -p |
Port published on pod member | Move -p to podman pod create |
podman port empty on host network |
Expected — no NAT publish layer | Connect to the port nginx binds on the host |
| Wrong client IP in rootless bridge logs | rootlessport does not preserve source IP |
Use default pasta or rootless_port_forwarder="pasta" when Pesto is supported |
bind: permission denied rootless low port |
Unprivileged port threshold | Use high port or privileged-port guide |
References
Summary
You mapped container ports to the host with -p HOST:CONTAINER, bound listeners on all interfaces, loopback only, or a specific host IP, and let Podman assign ephemeral host ports when the left side is omitted. TCP is default; /udp and matching ranges extend the same syntax. -P publishes every image EXPOSE port to a random host port — metadata versus real publication are different steps.
podman port is how you verify what actually landed, including podman port --all across running containers. Publishing requires a separate network namespace — bridge and pasta support it; host, none, macvlan, and ipvlan do not follow the same model. In pods, declare -p on podman pod create, not on containers that join the pod.
Rootless pasta preserves client source IPs; rootless bridge forwarding through rootlessport on Podman 5.x typically does not. Newer Podman/containers-common releases support rootless_port_forwarder="pasta" for bridge networks when the installed passt provides Pesto support. For failures after the syntax is correct, continue with the port-mapping troubleshooting and privileged-port guides rather than guessing firewall rules here.

