Nginx Bind Error: Fix Address Already in Use
Find the process on port 80 with ss -ltnp, stop the stale Nginx master or Apache clash, then restart Nginx cleanly and reliably..
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
- ✓SSH access to the Nginx host with sudo rights
- ✓Basic comfort with systemctl, ss, and log files
- ✓An Nginx install you can restart without customer impact
- Run ss -ltnp 'sport = :80' to name the process holding the port before you touch any config
- A stale Nginx master survives bad reloads: list it with ps aux | grep nginx, then stop it with nginx -s quit before starting fresh
- Apache on port 80 blocks Nginx cold: halt it with systemctl stop apache2 (or httpd), or move one server to its own port
- Twin listen 80 lines in two server blocks clash too: merge them, run nginx -t, then reload
Think of a parking space with one spot and two cars. Port 80 is that spot, and only one program can park there at a time. When Nginx starts and finds Apache — or a forgotten copy of itself — already parked on 80, it refuses to double-park and shuts down with this error. The fix isn't louder restarting. It's walking the lot, reading the license plate of the car in your spot with a single command, and moving exactly that car.
You push a config change, run systemctl restart nginx, and the deploy turns red: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use). Port 80 was yours yesterday. Nothing in the config looks wrong, nginx -t even passes, and every restart attempt fails the same way. Meanwhile the site is down or stuck on the old config, and the pressure to just reboot the box keeps growing.
The trap is that this error is never about your config syntax. It's about a port that already has an owner: a stale Nginx master that survived a reload, an Apache instance nobody remembered, a second server block with its own listen 80, or a container fighting the host for the same socket. Restarting Nginx can't evict any of them.
This guide walks the exact order that works: name the process on the port with ss, classify it (stale master, Apache, duplicate listen, container clash, or sub-1024 permission problem wearing a bind costume), remove precisely that blocker, and prove the port is free before Nginx touches it again.
Read the Log Line Before You Touch Anything
The emerg line is denser than it looks: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use). The address tells you which socket lost the race — 0.0.0.0:80 means all IPv4 interfaces on port 80, while [::]:80 is the IPv6 twin and 443 lines point at your TLS block. The errno in parentheses is the diagnosis: 98 (EADDRINUSE) means a living process owns that socket right now, and no config edit will move it. A 13 (EACCES) right after a user change or a container move means permission, not a clash, and sends you to capabilities instead of process hunting.
Pull the full context first. Run journalctl -u nginx --since '30 min ago' or tail /var/log/nginx/error.log to see whether every worker failed or just one, and whether 443 failed alongside 80 — a full-port failure points at a foreign daemon, while 80-only failure hints at a second server block or a sibling Nginx. Note the timestamp: if failures started exactly at a deploy, suspect a stale master from that reload; if they started at boot, suspect Apache enabled alongside Nginx.
Resist the two reflexes that waste the most time. Re-running restart in a loop never evicts the owner, and editing listen lines before naming the process often creates a second bug on top of the first. The log line plus one ss command is the whole diagnosis in most cases, and it takes under a minute.
Name the Squatter with ss, lsof, and fuser
ss is the fastest owner lookup on modern Linux: sudo ss -ltnp 'sport = :80' prints the listening socket plus the PID and process name in the last column. The -l flag limits output to listeners, -t to TCP, -n skips DNS lookups that slow you down during an outage, and -p exposes the process. Run the same for :443, because a clash on 80 often travels with one on 443, and fixing only half leaves TLS failing. If ss is missing on a minimal image, sudo netstat -ltnp is the older equivalent.
Cross-check with lsof and a live probe. sudo lsof -i :80 -S lists the command, PID, and user holding the port, which catches cases where ss output wraps badly. curl -sI http://127.0.0.1/ shows who's actually answering — an Apache default page versus your Nginx site instantly tells you which daemon won the port. fuser 80/tcp prints just the PIDs, handy for scripts that need to fail a deploy when the port is taken.
Record the PID, process name, and start time (ps -o pid,lstart,cmd -p <pid>) before killing anything. That triple is your evidence trail: it separates a stale Nginx master (same binary, old start time) from Apache (different binary) from a container proxy, and it stops you from killing the healthy master during a reload.
The Stale Master: When Nginx Blocks Itself
Nginx runs as one master plus worker processes, and reloads work by handing sockets from the old master to the new one. When a reload is interrupted — OOM killer, deploys racing each other, a config that passes -t but breaks at bind — the old master can survive while systemd records a failure. Its workers keep holding 80 and 443, and every fresh start dies on EADDRINUSE against Nginx itself. The cruel part: the site looks fine on stale config while every deploy fails.
Diagnose by comparing the PID file to reality. cat /run/nginx.pid (or /var/run/nginx.pid on older distros) shows the PID systemd believes in; ps -ef | grep nginx shows every master actually alive. Two masters, or a master whose PID differs from the file, is the smoking gun. Match the socket PIDs from ss to the old master's workers to be certain before acting.
Drain, don't nuke. sudo nginx -s quit asks the old master to finish in-flight requests and exit, which frees the ports without dropping connections. Only if quit hangs should you escalate to kill -TERM, and kill -9 is the last resort because orphaned workers can keep sockets open. After the port reads empty in ss, start once with systemctl start nginx and prove health with curl, not with another restart.
Apache on the Same Port: Ending the Roommate Fight
Fresh images and migrated LAMP boxes often ship with both Apache and Nginx installed and enabled. At boot they race for port 80; whoever starts second logs a bind failure and dies. Because both are legitimate web servers, the error looks like an Nginx bug when it's really a packaging default nobody reviewed. The tell is ss showing apache2 or httpd on :80 while your Nginx unit sits failed.
Confirm the roommate before evicting. systemctl status apache2 httpd 2>/dev/null shows which unit is active, and apachectl -S (or httpd -S) dumps Apache's own virtualhost map so you can see it genuinely wants port 80. Check whether Apache serves anything you need — legacy apps, certbot hooks, status pages — because stopping a server something depends on trades one outage for another. A quick curl of Apache's default page versus your site confirms which daemon clients actually reach.
Pick one outcome and make it permanent. If Nginx owns the box, stop and disable Apache so the next reboot doesn't replay the race. If both must coexist, move Apache's Listen to 8080 (ports.conf or httpd.conf) and point Nginx upstream at it, which turns a clash into a proxy tier. Either way, encode the winner in systemd enables and in your image build, or the fight returns on the next fresh boot.
Duplicate listen Lines Across Server Blocks
Nginx can also collide with itself through config: two server blocks each declaring listen 80 (plus the IPv6 twin listen [::]:80) with conflicting flags. The usual trigger is adding sites-enabled/new-app while default still carries listen 80 default_server, or copy-pasting a block and forgetting the original. The new master reads both, tries to bind twice, and dies — while nginx -t happily passes, because syntax is fine and only the bind is ambiguous.
Dump the effective config, not the files. sudo nginx -T prints the merged config Nginx actually loads, including every include chain, which is the only view that shows both listen lines together. Pipe it through grep -n 'listen' to list every bind directive with file context. Two default_server flags on one port, or identical listen plus server_name pairs that overlap, mark the conflict. Also check for an accidental double include of the same site file, which duplicates every directive silently.
Merge down to one owner per socket. Keep listen 80 default_server on exactly one block (usually the default catch-all) and plain listen 80 on the rest, with distinct server_name values routing traffic. When moving a site to TLS, keep port 80 only as a redirect stub or drop it. Validate with nginx -t and apply with reload, then confirm both 80 and 443 answer in the logs.
Sub-1024 Ports and Capabilities in Containers
Ports below 1024 are privileged: binding them requires root or the CAP_NET_BIND_SERVICE capability. When Nginx runs as an unprivileged user, in a rootless container, or under a tightened Docker security profile, the log shows bind() failed (13: Permission denied) instead of 98. The fix isn't freeing the port — it's granting the right to take low ports. Misreading 13 as 98 sends you hunting a squatter that doesn't exist.
Containers add a second collision surface. A host Nginx plus docker run -p 80:80, or two Compose services publishing 80, collide at the Docker userland proxy with the same EADDRINUSE text. docker ps --format with Ports exposes every published mapping, and ss on the host shows docker-proxy holding the socket. The container's Nginx config is innocent; the mapping is the bug.
Fix the right layer. For capabilities, run the container with --cap-add=NET_BIND_SERVICE, grant the binary with setcap on bare metal, or simply listen on 8080 and map it outward. For mapping clashes, give each service its own host port and let one Nginx (host or edge container) reverse-proxy to the rest. After any change, verify from outside the box with curl against the public address, since localhost inside the container lies about what's reachable.
A Stale Master Held Port 80 Through 11 Deploys and Faked a Config Bug
bind() to 0.0.0.0:80 failed (98: Address already in use). The site stayed up on the old config, which made it look like the deploy had been skipped rather than failed. Eleven restarts over two hours all failed identically, and nginx -t reported syntax ok on every attempt, so the team concluded the new config was somehow unbindable and started reverting it.- A passing nginx -t never clears a bind failure — it doesn't bind ports. Treat config tests and port ownership as two separate checks in every deploy.
- Always name the port owner with ss before editing config. The log line tells you the address; only ss tells you the process, and the process is the fix.
- Stale PID files lie to systemd. Compare the PID file against live processes in your deploy script so an orphaned master fails loudly instead of faking a config bug.
bind() failed, and you don't know what owns the port| File | Command / Code | Purpose |
|---|---|---|
| bind-first-look.sh | sudo nginx -t | Read the Log Line Before You Touch Anything |
| name-the-owner.sh | sudo ss -ltnp 'sport = :80' | Name the Squatter with ss, lsof, and fuser |
| evict-stale-master.sh | cat /run/nginx.pid | The Stale Master |
| end-apache-clash.sh | systemctl status apache2 httpd 2>/dev/null | head -n 20 | Apache on the Same Port |
| sites-available | server { | Duplicate listen Lines Across Server Blocks |
| container-port-triage.sh | docker ps --format 'table {{.Names}}\t{{.Ports}}' | Sub-1024 Ports and Capabilities in Containers |
Key takeaways
Common mistakes to avoid
5 patternsLooping systemctl restart without naming the port owner
Using kill -9 on the Nginx master first
Editing a site file Nginx never loads
Treating errno 13 like errno 98
Fixing the clash but not the boot order
Interview Questions on This Topic
Nginx fails with bind() (98: Address already in use). What's your first command?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
That's Networking. Mark it forged?
5 min read · try the examples if you haven't