Fix Gunicorn "Permission denied" Binding to Port 80

Fix Gunicorn Connection in use or Permission denied when binding to port 80 or 443: bind Gunicorn to a high port or Unix socket behind Nginx, or grant CAP_NET_BIND_SERVICE safely instead of running as root.

Problem statement

Starting Gunicorn on the standard web port fails:

[ERROR] Can't connect to ('0.0.0.0', 80)
[ERROR] Retrying in 1 second.
...
PermissionError: [Errno 13] Permission denied

On Linux, ports below 1024 are privileged: only root, or a process with the CAP_NET_BIND_SERVICE capability, can bind them. Gunicorn running as an ordinary user, as it should, cannot open port 80 or 443. The standard fix is not to make Gunicorn bind those ports at all: put Nginx or Caddy on 80 and 443 and let Gunicorn listen on a Unix socket or a high local port.

Why port 80 is refusedGunicorn startsas user www-dataBinds :80privileged portKernel checksroot or capabilityEACCESpermission deniedPorts below 1024 need root or CAP_NET_BIND_SERVICE; a reverse proxy avoids the need entirely.
  1. Bind Gunicorn to a socket or high port: --bind unix:/run/gunicorn/gunicorn.sock or --bind 127.0.0.1:8000.
  2. Put Nginx (or Caddy) on ports 80 and 443 and proxy to Gunicorn.
  3. If Gunicorn must face the network directly (for example inside a container behind a load balancer), use port 8000 and map it.
  4. Only if you really need Gunicorn on 80: grant AmbientCapabilities=CAP_NET_BIND_SERVICE in its systemd unit.
  5. Never run Gunicorn as root to get past this error.

Step-by-step solution

1. Move Gunicorn off the privileged port

In the systemd unit:

[Service]
User=deploy
Group=www-data
RuntimeDirectory=gunicorn
ExecStart=/srv/app/.venv/bin/gunicorn config.wsgi:application \
    --workers 3 \
    --bind unix:/run/gunicorn/gunicorn.sock

RuntimeDirectory= creates /run/gunicorn owned by the service user on each start. A Unix socket is not reachable from the network at all, which is a security benefit, and avoids port conflicts between projects.

2. Let Nginx own 80 and 443

server {
    listen 80;
    server_name example.com;
    location / {
        proxy_pass http://unix:/run/gunicorn/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Nginx's master process starts as root precisely so it can bind privileged ports, then its workers drop to an unprivileged user. It also handles TLS, static files, slow clients, and buffering, all of which Gunicorn's sync workers are poor at.

who should listen on port 80?Gunicorn on :80✕needs root or a capability✕slow clients tie up sync workers✕no TLS or static file servingNginx on :80, Gunicorn on a socket✓Gunicorn runs unprivileged✓Nginx buffers slow clients✓TLS, static files, limits in NginxThe proxy is why Gunicorn never needs privileged portsdjango-deployment.com
Gunicorn's own documentation recommends running it behind a proxy.

3. Containers: use a high port inside

In Docker, bind Gunicorn to 0.0.0.0:8000 inside the container and publish or route to it:

USER app
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
services:
  web:
    expose: ["8000"]       # reachable by the proxy container

Behind a cloud load balancer or Kubernetes Service, the external port 80/443 maps to container port 8000; Gunicorn never needs to bind 80. Recent Docker versions also set net.ipv4.ip_unprivileged_port_start=0 inside containers, but relying on that couples you to the runtime.

4. If you must: grant the capability

For a single-binary setup with no proxy, systemd can give just the bind capability to a non-root user:

[Service]
User=deploy
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ExecStart=/srv/app/.venv/bin/gunicorn config.wsgi:application --bind 0.0.0.0:80

Avoid setcap on the Python binary itself: it grants the capability to every script run with that interpreter.

5. Do not run as root

sudo gunicorn ... --bind 0.0.0.0:80 works, and gives every Django request, template, and dependency root privileges. Any remote code execution bug then owns the whole server. Gunicorn's --user option drops privileges after binding, but a proxy is simpler and safer.

where does Gunicorn bind?What sits in front of Gunicorn?Nginx on the hostUnix socket/run/gunicorn/gunicorn.sockcontainer or LB0.0.0.0:8000mapped from 80/443nothing at allCAP_NET_BIND_SERVICElast resort, no rootEvery common setup keeps Gunicorn off privileged portsdjango-deployment.com
Pick the bind address from what sits in front of Gunicorn.

Verification check:

sudo ss -ltnp | grep -E ':80 |gunicorn'
ps -o user,cmd -C gunicorn | head -3
curl -sI http://example.com | head -1

Port 80 should belong to Nginx, Gunicorn processes should run as your app user, and the site should respond.

Explanation

The privileged port rule dates from multi-user Unix systems, where it stopped ordinary users from impersonating system services such as a web server. Linux still enforces it by default. The reverse-proxy pattern sidesteps it cleanly: only the proxy, a small and hardened program designed to start as root and drop privileges, binds public ports. Gunicorn and Django run as a dedicated user with no special privileges, which limits the damage of any application bug.

Edge cases / notes

  • "Address already in use" on port 80 is a different error: another process holds the port.
  • --user and --group options let Gunicorn bind as root then drop privileges, but master startup and config loading still run as root.
  • SELinux can deny binding non-standard ports even for allowed users; label the port with semanage port.
  • Socket permissions. Nginx must be able to read and write the socket; matching groups avoid the socket permission error.

Next steps

FAQ

Why can't Gunicorn use port 80?

Ports below 1024 require root or the CAP_NET_BIND_SERVICE capability, and Gunicorn should run as a normal user.

Is running Gunicorn as root acceptable?

No. It gives every request handler root privileges. Use a proxy or a capability instead.

Should Gunicorn use a socket or a port?

A Unix socket behind Nginx on the same host; a high TCP port in containers or behind a load balancer.

Do I need Nginx at all?

For production, a proxy handles TLS, static files, and slow clients far better than Gunicorn alone.

Keep going