Podman Port Mapping: Publish Container Ports with `-p`

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
netavark-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:

text
host:8080
Podman port forwarding
container:80

This 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:

text
-p HOST_PORT:CONTAINER_PORT

Run nginx with host port 8080 forwarded to container port 80:

bash
podman run -d --name podman-port-demo -p 8080:80 docker.io/library/nginx:alpine

Ask Podman which host port is bound:

bash
podman port podman-port-demo

Sample output:

output
80/tcp -> 0.0.0.0:8080

Read that line as “container TCP port 80 is reachable on all host IPv4 addresses at port 8080.” Confirm the service answers:

bash
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8080/

Sample output:

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:

bash
podman run -d --name port-all-if -p 8080:80 docker.io/library/nginx:alpine

Confirm the mapping lists 0.0.0.0:

bash
podman port port-all-if

Sample output:

output
80/tcp -> 0.0.0.0:8080

On the host, the listener covers all interfaces:

bash
ss -ltnp | grep ':8080'

Sample output:

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:

bash
podman run -d --name port-loop -p 127.0.0.1:8081:80 docker.io/library/nginx:alpine

The publish map should show loopback only:

bash
podman port port-loop

Sample output:

output
80/tcp -> 127.0.0.1:8081

ss confirms the narrower bind:

bash
ss -ltnp | grep ':8081'

Sample output:

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:

bash
podman run -d --name port-hostip -p 10.0.2.15:8082:80 docker.io/library/nginx:alpine

Verify Podman bound only that address:

bash
podman port port-hostip

Sample output:

output
80/tcp -> 10.0.2.15:8082

Traffic 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:

bash
podman run -d --name port-random -p 80 docker.io/library/nginx:alpine

Discover which host port Podman picked:

bash
podman port port-random

Sample output:

output
80/tcp -> 0.0.0.0:44003

Query one container port directly:

bash
podman port port-random 80

Sample output:

output
0.0.0.0:44003

You can also leave the host side empty while pinning the bind address:

bash
podman run -d --name port-random2 -p 127.0.0.1::80 docker.io/library/nginx:alpine

Query the assigned loopback port for container port 80:

bash
podman port port-random2 80

Sample output:

output
127.0.0.1:41295

Random 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:

bash
podman run -d --name port-udp -p 15353:53/udp docker.io/library/alpine:3.20 sleep 600

The mapping line should name UDP explicitly:

bash
podman port port-udp

Sample output:

output
53/udp -> 0.0.0.0:15353

Publish both protocols on the same host port when the service needs it:

bash
podman run -d --name port-both -p 15354:53/tcp -p 15354:53/udp docker.io/library/alpine:3.20 sleep 600

Both protocol lines should share host port 15354:

bash
podman port port-both

Sample output:

output
53/tcp -> 0.0.0.0:15354
53/udp -> 0.0.0.0:15354

Rootful 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:

bash
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:

bash
podman port port-range

Sample output:

output
8000/tcp -> 0.0.0.0:8000
8001/tcp -> 0.0.0.0:8001
8002/tcp -> 0.0.0.0:8002

Three 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:

bash
podman run -d --name port-pall -P docker.io/library/nginx:alpine

-P assigns a random host port for each exposed image port:

bash
podman port port-pall

Sample output:

output
80/tcp -> 0.0.0.0:34467

Compare that to the image metadata, which only documents the container port:

bash
podman inspect port-pall --format '{{json .Config.ExposedPorts}}'

Sample output:

output
{"80/tcp":{}}

Important distinction:

text
EXPOSE in Containerfile → image metadata only
-p / -P                 → actual host publication

EXPOSE 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:

bash
podman port podman-port-demo

One container port:

bash
podman port podman-port-demo 80

With explicit protocol:

bash
podman port podman-port-demo 80/tcp

Every running container with published ports:

bash
podman port --all

Sample output (trimmed):

output
725f8829cb80
80/tcp -> 0.0.0.0:8080
080cc917ba8f
80/tcp -> 127.0.0.1:8081
7e0073b18f59
80/tcp -> 10.0.2.15:8082

podman 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:

bash
podman run -d --name port-host --network host docker.io/library/nginx:alpine

With host networking there is nothing to list:

bash
podman port port-host

The 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:

bash
podman pod create --name web-pod -p 8090:80

Add nginx as a pod member — no extra -p on the container:

bash
podman run -d --pod web-pod --name web-pod-c1 docker.io/library/nginx:alpine

Member containers inherit the pod publish map:

bash
podman port web-pod-c1

Sample output:

output
80/tcp -> 0.0.0.0:8090

podman port web-pod fails because the pod is not a container — query a member instead. The service is reachable:

bash
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8090/

Sample output:

output
HTTP 200

Trying to add -p on a container that joins an existing pod is rejected:

bash
podman pod create --name bad-pod

Now try publishing on a container inside that pod:

bash
podman run -d --pod bad-pod -p 8091:80 --name bad-pod-c1 docker.io/library/nginx:alpine

Sample output:

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 pod

Pod 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:

bash
podman run -d --name port-rootless -p 8083:80 docker.io/library/nginx:alpine

Rootless pasta uses the same podman port output format:

bash
podman port port-rootless

Sample output:

output
80/tcp -> 0.0.0.0:8083

Pasta 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

IMPORTANT
On the tested Podman 5.8.2 host, rootless user-defined bridges use 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:

toml
[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.


Frequently Asked Questions

1. What is the Podman port mapping syntax?

Use -p HOST_PORT:CONTAINER_PORT to map a host port to a port inside the container. Example: -p 8080:80 forwards host 8080 to container port 80 over TCP by default. Omit the host port with -p 80 to let Podman pick a free ephemeral port.

2. Does EXPOSE in a Containerfile publish the port on the host?

No. EXPOSE only documents which ports the image expects to use. Actual host publication requires -p for explicit mapping or -P to publish all exposed ports to random host ports. Run podman port to see what is really bound.

3. Can I publish ports on a container inside a Podman pod?

No at the container level. Pod members share the pod network namespace, so publish ports when you create the pod with podman pod create -p. A container-level -p inside an existing pod is rejected.

4. Does Podman port forwarding work with --network host?

Host networking shares the host network stack, so there is no separate publish boundary and -p is not the normal model. Services listen directly on host addresses. Use podman port on bridge or pasta containers to inspect mappings.

5. Why does rootless bridge port forwarding show the wrong client IP?

Rootless containers on a user-defined bridge network use rootlessport by default on Podman 5.x. That forwarder does not preserve the original client source IP in application logs. Default rootless pasta networking preserves client IPs. Newer Podman/containers-common releases support rootless_port_forwarder="pasta" when the installed passt provides Pesto support.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)