Fix Rootless Podman Cannot Bind to Port 80 or 443

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
Applies to Any Linux host with rootless Podman
Privilege Rootless examples as podtest; sysctl, firewalld, and setcap demos need sudo
Scope Privileged host ports 80 and 443 with rootless Podman — reproduce pasta and rootlessport errors, kernel ip_unprivileged_port_start, sysctl persistence, reverse proxy pattern, firewalld forward-port, NET_BIND_SERVICE and setcap misconceptions, host networking limits, and fix selection. Does not cover general -p syntax, rootless setup, or full proxy/firewall tutorials.
Related guides Rootless Podman networking
Rootless Podman
Podman networking modes

Publishing -p 80:80 or -p 443:443 as a normal user often fails before your container starts. The error text depends on whether Podman uses pasta or rootlessport, but the underlying cause is the same: the host-side forwarding process is unprivileged and Linux blocks low ports by default.

This guide reproduces both error shapes, explains the kernel rule, and walks through fixes that keep the workload rootless. General port-mapping syntax lives in Podman port mapping.

The lab user is podtest for rootless examples.


Reproduce the rootless port 80 failure

Check the kernel threshold first:

bash
cat /proc/sys/net/ipv4/ip_unprivileged_port_start
output
1024

Publish host port 80 as a normal user with default pasta networking:

bash
podman run --rm -p 80:80 docker.io/library/alpine:3.20 echo ok
output
Error: pasta failed with exit code 1:
Failed to bind port 80 (Permission denied) for option '-t 80-80:80-80'

On Podman 5.x with pasta you may also see wording like:

text
Listen failed for HOST TCP port */80: Permission denied
Couldn't listen on requested TCP ports

Older tutorials often quote rootlessport instead:

text
rootlessport cannot expose privileged port ...
bind: permission denied

Both strings point at the same kernel restriction; the process that reports the error depends on the networking path — covered later in this page.


Why rootless Podman cannot bind port 80 by default

Linux enforces privileged ports through:

text
net.ipv4.ip_unprivileged_port_start

When the value is 1024:

text
0–1023   → privileged (need capability or root for bind)
1024+    → unprivileged

Rootless Podman publishes ports through a host-side helper — pasta on the default path in Podman 5, or rootlessport on some bridge setups. That helper runs as your login user, not host root.

Adding a capability inside the container does not change the host-side listener:

text
--cap-add NET_BIND_SERVICE   → container process only
host pasta/rootlessport      → still unprivileged on the host

That is why --cap-add NET_BIND_SERVICE is not the generic fix for -p 80:80.


Method 1: Lower ip_unprivileged_port_start

Read the current value:

bash
sysctl net.ipv4.ip_unprivileged_port_start
output
net.ipv4.ip_unprivileged_port_start = 1024

Allow unprivileged binds from port 80 upward (HTTP and HTTPS):

bash
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
output
net.ipv4.ip_unprivileged_port_start = 80

Retry the publish. Stop any existing service on port 80 first if something like httpd already listens there:

bash
podman run --rm -p 80:80 docker.io/library/alpine:3.20 echo ok
output
ok

To allow HTTPS only while keeping port 80 privileged, use 443 instead:

bash
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=443

HTTPS on port 443 succeeds at that threshold:

bash
podman run --rm -p 443:443 docker.io/library/alpine:3.20 echo ok
output
ok

Port 80 still fails when the threshold is 443:

bash
podman run --rm -p 80:80 docker.io/library/alpine:3.20 echo ok
output
Failed to bind port 80 (Permission denied) for option '-t 80-80:80-80'

Restore the lab default when finished:

bash
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=1024

Make the sysctl persistent

Create a drop-in file:

bash
sudo tee /etc/sysctl.d/99-rootless-podman-ports.conf <<'EOF'
net.ipv4.ip_unprivileged_port_start=80
EOF

Apply all sysctl drop-ins:

bash
sudo sysctl --system

Verify:

bash
sysctl net.ipv4.ip_unprivileged_port_start

Security scope matters: ip_unprivileged_port_start is a per-network-namespace setting. Changing it with the host sysctl command changes the host network namespace, so ordinary unprivileged host processes — not only Podman — can bind ports at or above the new threshold. After setting 80, any unprivileged user or daemon in that namespace can bind port 80 and above unless another policy blocks it. Treat it as a host-namespace decision, not a Podman-only permission grant.


Method 2: Run on a high port behind a reverse proxy

A common production pattern keeps the application rootless on a high port and lets a root-managed proxy own 80 and 443.

Publish only on localhost at 8080:

bash
podman run -d --name web-app -p 127.0.0.1:8080:8080 IMAGE

A root-managed reverse proxy listens on:

text
80 / 443

and forwards to:

text
127.0.0.1:8080
text
Client
   │ 80/443
root-managed proxy (nginx, Caddy, HAProxy)
   │ 8080
rootless Podman app

Advantages:

  • container stays rootless
  • TLS termination can live in one place
  • no global sysctl change
  • multiple apps can route by hostname

This page does not include a full proxy tutorial — only the split between privileged edge ports and an unprivileged container port.


Method 3: Redirect port 80 or 443 with the host firewall

Another pattern publishes the container on a high port and uses the host firewall to redirect inbound traffic:

text
host port 80
firewalld local port redirect
host port 8080
rootless Podman listener

Run the container on 8080:

bash
podman run -d --name web-app \
  -p 8080:80 \
  docker.io/library/nginx:alpine

Unlike the reverse-proxy method, this example publishes 8080 on the host rather than only on 127.0.0.1, because firewalld redirects the incoming packet to another local port instead of opening a new connection to the loopback listener.

On RHEL 10 with firewalld active, add a forward rule. For same-host redirection, use toport without toaddr:

bash
sudo firewall-cmd --zone=public \
  --add-forward-port=port=80:proto=tcp:toport=8080
output
success

Make it persistent across reboots when you are satisfied with the rule:

bash
sudo firewall-cmd --zone=public \
  --add-forward-port=port=80:proto=tcp:toport=8080 \
  --permanent
bash
sudo firewall-cmd --reload

Remove a test rule:

bash
sudo firewall-cmd --zone=public \
  --remove-forward-port=port=80:proto=tcp:toport=8080

If you made it permanent, also remove the permanent rule:

bash
sudo firewall-cmd --zone=public \
  --remove-forward-port=port=80:proto=tcp:toport=8080 \
  --permanent

Trade-offs:

  • requires root or admin firewall access
  • application container stays rootless
  • host firewall becomes part of the operational contract
  • localhost traffic may not follow the same DNAT path as external clients on every distribution — verify from the client perspective you care about

Method 4: Use systemd socket activation

Some applications support systemd socket activation: a system-level socket unit owns privileged port 80 or 443, and systemd passes the listening file descriptor to an unprivileged service process.

That works only when the application explicitly supports inherited sockets. Most container images expect to call bind() themselves, so this path is specialized compared with a reverse proxy. Treat it as an advanced alternative, not the default recommendation.


Why --cap-add NET_BIND_SERVICE does not fix host port publishing

With ip_unprivileged_port_start still at 1024, adding the capability inside the container does not help:

bash
podman run --rm --cap-add NET_BIND_SERVICE -p 80:80 docker.io/library/alpine:3.20 echo ok
output
Error: pasta failed with exit code 1:
Failed to bind port 80 (Permission denied) for option '-t 80-80:80-80'

NET_BIND_SERVICE changes what the container process may do inside its namespace. The pasta process creating the host publish socket still runs as your user and still hits the kernel low-port rule.


Why setcap on rootlessport is not a reliable fix

Many older articles recommend:

bash
sudo setcap cap_net_bind_service=+ep /usr/libexec/podman/rootlessport

On Podman 5 rootless defaults, pasta handles publishing — not rootlessport. Changing rootlessport cannot fix a pasta failure.

Even on a bridge network where rootlessport runs, the capability did not help on this host. After applying setcap:

bash
getcap /usr/libexec/podman/rootlessport
output
/usr/libexec/podman/rootlessport cap_net_bind_service=ep

Publishing port 80 still failed:

bash
podman run --rm --network bridge -p 80:80 docker.io/library/alpine:3.20 echo ok
output
Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied

Remove the lab capability before you finish:

bash
sudo setcap -r /usr/libexec/podman/rootlessport

Do not leave modified capabilities on system binaries.


pasta error vs rootlessport error

Networking path Typical error source Example text
Default pasta (Podman 5 rootless) pasta host bind Failed to bind port 80 (Permission denied) or Listen failed for HOST TCP port */80: Permission denied
Rootless bridge / explicit --network bridge rootlessport rootlessport cannot expose privileged port 80 ... bind: permission denied

The root cause is identical — unprivileged host bind below ip_unprivileged_port_start. Only the reporting process differs. Search results may show either string depending on Podman version and network mode.

Force the rootlessport path to see the older wording:

bash
podman run --rm --network bridge -p 80:80 docker.io/library/alpine:3.20 echo ok
output
Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied

--network=host has the same kernel restriction

Host networking does not bypass the low-port rule for a rootless process. If the application inside the container tries to listen on port 80:

bash
podman run --rm --network host docker.io/library/alpine:3.20 nc -l -p 80
output
nc: bind: Permission denied

--network=host is not a workaround for privileged port publishing.


Should you set the threshold to 0, 80, or 443?

Value Effect
0 unprivileged processes may bind any port — rarely appropriate
80 ports 80 and above are unprivileged — covers both HTTP and HTTPS
443 ports 443 and above only — port 80 stays privileged
Requirement Set threshold to
HTTPS only (443+) 443
HTTP and HTTPS (80 and 443) 80
every port 0 (avoid unless you understand the exposure)

Do not set 0 when you only need web ports.


Which fix should you use?

Method Scope Good fit
lower ip_unprivileged_port_start host network namespace dedicated server where broad low-port access is acceptable
reverse proxy per service typical web workloads — recommended default
firewall redirect host networking rule fixed port translation without changing sysctl
systemd socket activation app-specific software that supports inherited sockets
run Podman rootful changes privilege model workload genuinely needs rootful operation — see Rootless vs rootful Podman

No single method is universally best. Development containers can publish 8080 during testing. Production hosts usually pair rootless apps on high ports with a root-managed edge proxy.


References


Summary

Rootless Podman cannot publish host ports below ip_unprivileged_port_start — usually 1024 — because pasta or rootlessport runs as your user when creating the host listener. Podman 5 with pasta reports Failed to bind port 80 (Permission denied); bridge mode may show the older rootlessport cannot expose privileged port text instead. The kernel rule is the same in both cases.

Fixes fall into three practical buckets. Lowering net.ipv4.ip_unprivileged_port_start to 80 works but changes the host network namespace, so ordinary unprivileged processes — not just your container — can bind at or above the new threshold. Publishing on 127.0.0.1:8080 and fronting the app with a root-managed reverse proxy is the cleaner pattern for most web workloads. A firewalld forward-port rule can redirect inbound 80 to a high port while the container stays rootless.

--cap-add NET_BIND_SERVICE and setcap on rootlessport do not solve pasta publishing failures — the host-side helper remains unprivileged. --network=host does not bypass the restriction either. Pick sysctl tuning only when you accept the host-namespace exposure; otherwise use a high port plus proxy or redirect and keep the workload rootless.


Frequently Asked Questions

1. Why cannot rootless Podman bind to port 80?

Linux reserves host ports below the ip_unprivileged_port_start threshold, usually 1024, for privileged processes. Rootless Podman runs pasta or rootlessport as your normal user, so the host-side listener on port 80 fails with permission denied unless you change the sysctl, use a high port, or forward traffic with a proxy or firewall rule.

2. Does --cap-add NET_BIND_SERVICE fix rootless port 80 publishing?

No. NET_BIND_SERVICE inside the container affects the container process namespace. The host-side pasta or rootlessport process that creates the published socket still runs as an unprivileged user and cannot bind port 80 when ip_unprivileged_port_start is 1024.

3. What sysctl value allows both port 80 and 443?

Set net.ipv4.ip_unprivileged_port_start to 80. A value of 443 allows 443 and above but still blocks port 80 because 80 is below 443. Setting 0 allows every port and is rarely appropriate.

4. Is setcap on rootlessport a reliable fix?

No. On Podman 5 the default rootless path uses pasta, not rootlessport, so modifying rootlessport does not help pasta failures. Even on bridge networks where rootlessport runs, cap_net_bind_service on the binary did not fix port 80 publishing in testing on RHEL 10.2.

5. Is a reverse proxy better than lowering ip_unprivileged_port_start?

For typical web workloads, yes. Publishing the rootless app on 127.0.0.1:8080 and letting a root-managed proxy listen on 80 and 443 avoids a system-wide sysctl change, centralizes TLS, and keeps the container rootless.
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)