Node.js — Error: listen EACCES When Binding Port 443 or 80
Resolve listen EACCES on privileged ports in Node.js by identifying the capability boundary, then use setcap, port redirection, or an unprivileged reverse proxy.
Symptoms
- Error: listen EACCES: permission denied 0.0.0.0:443 at Server.setupListenHandle on startup
- The app runs fine locally on port 3000 but fails in the container or VM when PORT=443
- The same error appears for port 80 while 8080 works
Possible causes
- The process runs as an unprivileged user and Linux requires CAP_NET_BIND_SERVICE for ports below 1024
- A container drops capabilities by default, so even a root-adjacent user cannot bind 80 or 443
- SELinux or AppArmor policy denies the bind for the service user
- Another hardening layer (systemd RestrictAddressFamilies or a non-root PodSecurity policy) blocks the socket option
Diagnosis steps
Confirm the identity and capability set
id && grep Cap /proc/self/statusInterpretation
If the effective user is not root and CapEff lacks bit 12 (CAP_NET_BIND_SERVICE), the kernel will refuse any bind below 1024.
Verify the port itself is free
ss -ltnp | grep -E ":443|:80"Interpretation
EACCES means permission, not occupancy; an empty listing rules out EADDRINUSE and points back to capabilities.
Check the platform policy layer
getenforce 2>/dev/null; kubectl get pod -o jsonpath="{.spec.securityContext}"Interpretation
SELinux enforcing mode or a container security context can deny the bind independently of Unix permissions.
Root cause
The Node process attempted to bind a port below 1024 without CAP_NET_BIND_SERVICE. Unlike EADDRINUSE, which signals occupancy, EACCES signals a policy boundary: the kernel refuses the bind for this identity in this security context.
Fix
Prefer running the app on an unprivileged port such as 8443 with TLS terminated by nginx or a load balancer. When direct binding is required, grant the capability to the binary with setcap cap_net_bind_service=+ep, or start as root and drop privileges with setuid after listen, or use an ambient capability in the container securityContext.
Risk note
Running the whole service as root to bypass EACCES widens the blast radius far beyond the bind call. Grant the single capability or proxy the port instead.
Verify the fix
Restart the service under the production user and confirm it logs listening on 443, then curl https://localhost to complete a full handshake and response.
Symptom walkthrough
The error text carries the classification inside it:
Error: listen EACCES: permission denied 0.0.0.0:443
at Server.setupListenHandle [as _listen2] (node:net:1874:18)
EACCES is a permission refusal, not an occupancy conflict. The diagnostic shortcut is to try the same code on port 8443: if it binds, the code is fine and the identity is the problem. Confirm with capability inspection:
id
grep Cap /proc/self/status
An unprivileged uid with no CAP_NET_BIND_SERVICE bit (bit 12 of CapEff) guarantees the kernel refusal.
Fix in practice
Pick the option that matches your deployment shape. The cleanest is an unprivileged app behind a proxy that owns 443:
internet -> nginx :443 (root, master) -> app :3000 (app user)
When the app must own the port, grant the narrow capability to the exact binary:
sudo setcap cap_net_bind_service=+ep $(readlink -f $(which node))
In Kubernetes, set capabilities in the container securityContext:
securityContext:
capabilities:
add: ['NET_BIND_SERVICE']
Re-check after any Node upgrade, because setcap binds to the file identity and a new binary starts without it.
Verification
Restart under the production user and watch for the listening log line on 443, then complete an end-to-end curl https://localhost/health. Confirm the capability is still scoped: capsh –print should show the single added capability, and dropping back to an unmodified binary should reproduce the original EACCES, proving the fix lives exactly where you think it does.