EACCES high
Node.js Error: listen EACCES: permission denied 0.0.0.0:80
A non-root Node process cannot bind a privileged port below 1024 like 80 or 443.
What you see
Error: listen EACCES: permission denied 0.0.0.0:80
at Server.setupListenHandle [as _listen2] (node:net:1789:21)
at listenInCluster (node:net:1865:12)
code: 'EACCES', errno: -13, syscall: 'listen', address: '0.0.0.0', port: 80 What’s actually happening
server.listen(80) throws immediately on startup under a normal user account. Run the exact same code with sudo and it binds fine — that's the tell. On Linux, ports under 1024 are privileged and need either root or an explicit capability. Don't fix this by running the whole app as root; that hands an internet-facing process full system rights.
Common causes
- The process runs as an unprivileged user (the correct, safe setup) and tries to bind 80 or 443 directly.
- Binding 443 in Node for TLS termination instead of putting a reverse proxy in front.
- A systemd service file missing the capability grant needed to open a low port as a non-root user.
- On a shared host, you simply aren't allowed to claim 80/443 at all — they belong to the platform's web server.
- Confusing EACCES (permission) with EADDRINUSE (port taken) — different fix entirely.
How to fix it
- Put nginx in front and listen high (recommended)Bind Node to 3000 and let nginx own 80/443: a server block with listen 80; proxy_pass http://127.0.0.1:3000;. nginx also terminates TLS, so your Node process never touches a privileged port or a certificate.
- Grant the capability to the Node binarysudo setcap 'cap_net_bind_service=+ep' $(which node) lets any Node process bind low ports without root. Re-run it after a Node upgrade — the cap is on the binary's inode and is lost when the file is replaced.
- Use authbind for a per-user grantsudo apt install authbind; sudo touch /etc/authbind/byport/80; sudo chown youruser /etc/authbind/byport/80; sudo chmod 755 /etc/authbind/byport/80. Then launch with authbind --deep node server.js. More granular than setcap — scoped to one port and user.
- Grant the capability in systemd insteadIn the unit's [Service] section: AmbientCapabilities=CAP_NET_BIND_SERVICE and NoNewPrivileges=true, running as User=youruser. systemctl daemon-reload then restart. The service binds 80 with zero root code paths.
- Last resort — redirect with the firewallKeep Node on 3000 and route the traffic: sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 3000. Works, but it's opaque six months later, so document it.
Stop it recurring
Run the app as a non-root user on a high port behind nginx, or grant CAP_NET_BIND_SERVICE via systemd — never run the whole process as root.
Related errors