Home › DevOps › Nginx Bind Error: Fix Address Already in Use
Beginner 5 min · September 23, 2026

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..

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 11 min
  • ✓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
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • 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
✦ Definition~90s read
What is Nginx Bind Address in Use Fix?

On Linux only one socket can bind a given IP-plus-port pair at a time (unless SO_REUSEPORT is set, which stock Nginx doesn't use for port 80). When Nginx starts or reloads, each worker process calls the bind() system call to claim the listen sockets from the config.

★
Think of a parking space with one spot and two cars.

If anything already owns 0.0.0.0:80 — another daemon, a leftover Nginx master, or Nginx itself started twice — the kernel refuses with EADDRINUSE, error 98. Nginx logs it as an [emerg] line and exits instead of serving half its config.

Four owners cause nearly every case. A stale master happens when a reload or restart leaves the old master running: the new master can't bind, dies, and the old one keeps serving stale config. Apache is the classic roommate conflict on LAMP-era boxes and fresh images that ship both servers enabled.

Duplicate listen directives across two server blocks (often default plus a new site file) make Nginx fight itself. In containers, a host Nginx plus a published -p 80:80 container, or two Compose services mapped to 80, collide at the Docker proxy layer with the same message.

Two facts save hours. First, nginx -t validates syntax but never binds ports, so a passing config test proves nothing about this error. Second, a nearby lookalike — bind() failed (13: Permission denied) — is a different bug: binding ports under 1024 needs root or the CAP_NET_BIND_SERVICE capability.

EADDRINUSE means something owns the port; EACCES means you're not allowed to take it. Check the errno number in the log line before you act.

Plain-English First

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.

bind-first-look.shBASH
1
2
3
4
5
sudo nginx -t
sudo tail -n 30 /var/log/nginx/error.log
sudo journalctl -u nginx --since '30 min ago' --no-pager | tail -n 30
sudo ss -ltnp 'sport = :80'
sudo ss -ltnp 'sport = :443'
📊 Production Insight
On-call teams that quote the errno number in the incident thread resolve these faster, because 98 and 13 send responders down completely different paths from the first message.
🎯 Key Takeaway
Errno 98 means a process owns the port; errno 13 means permission. Read the number, check both 80 and 443, then hunt the owner.

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.

name-the-owner.shBASH
1
2
3
4
5
6
sudo ss -ltnp 'sport = :80'
sudo ss -ltnp 'sport = :443'
sudo lsof -i :80 -S
sudo fuser 80/tcp 443/tcp
curl -sI http://127.0.0.1/ | head -n 8
ps -o pid,lstart,cmd -p $(sudo fuser 80/tcp 2>/dev/null)
📊 Production Insight
Probing with curl before killing has saved more than one team from stopping the healthy master: the Server header names the winner, so you evict the loser with confidence.
🎯 Key Takeaway
ss names the PID, lsof confirms the user, curl names the winner. Collect all three before you stop anything.

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.

evict-stale-master.shBASH
1
2
3
4
5
6
7
8
cat /run/nginx.pid
ps -ef | grep -E 'nginx: master' | grep -v grep
sudo nginx -s quit
sleep 3
sudo ss -ltnp 'sport = :80' || echo 'port 80 free'
sudo systemctl start nginx
sudo systemctl is-active nginx
curl -sI http://127.0.0.1/ | head -n 5
📊 Production Insight
Deploy pipelines that compare the PID file against live masters before starting Nginx turn this whole class of outage into a clean pre-deploy failure with a useful message.
🎯 Key Takeaway
Two masters means Nginx is blocking itself. Quit the old master gracefully, verify the port is empty, then start exactly once.

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.

end-apache-clash.shBASH
1
2
3
4
5
6
systemctl status apache2 httpd 2>/dev/null | head -n 20
sudo apachectl -S 2>/dev/null || sudo httpd -S 2>/dev/null
sudo grep -rn '^Listen' /etc/apache2/ports.conf /etc/httpd/conf/httpd.conf 2>/dev/null
sudo systemctl stop apache2 httpd 2>/dev/null
sudo systemctl disable apache2 httpd 2>/dev/null
sudo systemctl start nginx && curl -sI http://127.0.0.1/ | head -n 3
📊 Production Insight
Base-image audits that assert only one web server is enabled have killed this outage at one shop: the image build fails if both units are enabled, so it can never reach production.
🎯 Key Takeaway
If ss shows Apache on 80, decide the winner once: disable the loser or re-port it, and lock that choice into the image.

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.

sites-available/merged-listens.confNGINX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Exactly ONE block owns 'default_server' per port.
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

server {
    listen 80;
    listen [::]:80;
    server_name app.example.com;
    location / { proxy_pass http://127.0.0.1:3000; }
}

# Diagnose duplicates with:
# sudo nginx -T | grep -n 'listen'
# sudo nginx -t && sudo nginx -s reload
⚠ Only One default_server Per Port
Two blocks with listen 80 default_server is the most common self-clash. Keep the flag on your catch-all block only, and give every other block a plain listen plus a unique server_name.
📊 Production Insight
Teams that run nginx -T | grep listen in CI catch duplicate binds before deploy: the pipeline diffs the listen map against the previous release and blocks unexpected new owners of port 80.
🎯 Key Takeaway
nginx -T shows the merged truth. One default_server per port, unique server names, then test and reload.

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.

container-port-triage.shBASH
1
2
3
4
5
6
7
8
docker ps --format 'table {{.Names}}\t{{.Ports}}'
sudo ss -ltnp | grep -E ':80 |:443 '
# Capability fix on bare metal:
sudo setcap 'cap_net_bind_service=+ep' $(which nginx)
# Or run unprivileged on 8080 and map outward:
# docker run -d -p 80:8080 my-nginx
# server { listen 8080; }
curl -sI http://$(hostname -I | awk '{print $1}')/ | head -n 3
📊 Production Insight
Edge boxes that moved Nginx to 8080 behind a host-level redirect cut their permission incidents to zero: unprivileged Nginx plus a static iptables redirect is simpler than capabilities spread across a fleet.
🎯 Key Takeaway
Errno 13 is capabilities, errno 98 is a clash. Fix mappings and caps at the right layer, then verify from outside.
● Production incidentPOST-MORTEMseverity: high

A Stale Master Held Port 80 Through 11 Deploys and Faked a Config Bug

Symptom
After a routine config deploy, systemctl restart nginx failed with 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.
Assumption
Because the error names the listen address, everyone assumed the new server block had a bad listen directive. Two engineers diffed the config against the last known good copy and found one added server block. They removed it, reverted, re-pushed — same failure. Nobody ran ss or ps because the box had been rebooted the previous week and was assumed clean.
Root cause
A failed reload days earlier had orphaned the old Nginx master: its PID file was stale, so systemctl thought Nginx was down while the old master and its workers still held port 80 and 443. Every new start died on EADDRINUSE against Nginx itself. The added server block was innocent. One ss -ltnp line showing nginx PIDs older than the deploy would have ended it in a minute.
Fix
They listed listeners with ss -ltnp, matched the PIDs to the orphaned master via ps, drained it with nginx -s quit, confirmed the ports were free, then started Nginx once and verified with curl. They also added a pre-deploy check that compares the PID file against running masters and fails the pipeline on mismatch.
Key lesson
  • 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.
Production debug guideFive checks in order — name the owner, classify it, evict exactly that process.5 entries
Symptom · 01
nginx emerg bind() failed, and you don't know what owns the port
→
Fix
Name it: run sudo ss -ltnp 'sport = :80' and sudo ss -ltnp 'sport = :443'. The last column gives PID and process name. Cross-check with sudo lsof -i :80 -S and curl -sI http://127.0.0.1/ to see who's actually answering. Don't edit config until a PID is on screen.
Symptom · 02
The owner is nginx itself — a stale master survived a reload
→
Fix
Run ps -ef | grep nginx to find the old master PID and cat /run/nginx.pid to spot a stale PID file. Drain it with sudo nginx -s quit (or sudo kill -QUIT <old-master-pid>), wait for workers to exit, confirm with sudo ss -ltnp 'sport = :80' showing nothing, then sudo systemctl start nginx and curl -sI localhost.
Symptom · 03
The owner is apache2 or httpd on the same port
→
Fix
Confirm with systemctl status apache2 httpd 2>/dev/null and sudo ss -ltnp showing httpd/apache2 on :80. Stop the loser with sudo systemctl stop apache2 (or httpd) and sudo systemctl disable apache2 if Nginx owns this box. If both must live here, move one Listen port in ports.conf/httpd.conf, then sudo nginx -t && sudo systemctl reload nginx.
Symptom · 04
No foreign process, yet Nginx still fails — suspect duplicate listen lines
→
Fix
Dump the live config with sudo nginx -T and run sudo nginx -T | grep -n 'listen' to find twin listen 80 directives. Merge duplicates so one server block carries listen 80 default_server, validate with sudo nginx -t, then reload with sudo nginx -s reload and re-check error.log.
Symptom · 05
Host and container (or two containers) fight over port 80, or the log shows errno 13
→
Fix
Run docker ps --format '{{.Names}} {{.Ports}}' to find the published 80, then change one mapping (e.g. -p 8080:80) or stop the host Nginx. If the log says 13: Permission denied instead of 98, you're unprivileged on a sub-1024 port: run as root, or grant it with sudo setcap 'cap_net_bind_service=+ep' $(which nginx).
Nginx Bind Failures Compared
Root CauseHow to ConfirmFixPrevention
Stale Nginx master holds 80/443ss shows nginx PIDs older than the deploy; PID file mismatches psnginx -s quit the old master, verify free, start oncePre-deploy check comparing PID file to live masters
Apache (or httpd) owns port 80ss shows apache2/httpd; systemctl status confirms the unitStop and disable it, or move its Listen to 8080Image builds enabling exactly one web server
Duplicate listen across server blocksnginx -T | grep listen shows twins or two default_serversMerge to one default_server, unique server_names, reloadCI gate diffing the listen map per release
Container mapping clash or missing capabilitydocker ps shows published 80 twice; or log errno is 13Remap host ports; add NET_BIND_SERVICE or use 8080One edge proxy per host; Compose port review in CI
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
bind-first-look.shsudo nginx -tRead the Log Line Before You Touch Anything
name-the-owner.shsudo ss -ltnp 'sport = :80'Name the Squatter with ss, lsof, and fuser
evict-stale-master.shcat /run/nginx.pidThe Stale Master
end-apache-clash.shsystemctl status apache2 httpd 2>/dev/null | head -n 20Apache on the Same Port
sites-availablemerged-listens.confserver {Duplicate listen Lines Across Server Blocks
container-port-triage.shdocker ps --format 'table {{.Names}}\t{{.Ports}}'Sub-1024 Ports and Capabilities in Containers

Key takeaways

1
Only one socket owns an IP-plus-port pair
name that owner with ss before changing anything.
2
nginx -t never binds ports, so a passing test says nothing about bind failures.
3
Stale masters block fresh starts
quit the old master gracefully, verify, then start once.
4
Errno 98 hunts processes; errno 13 fixes capabilities or the listen user.
5
One default_server per port, unique server_names, merged via nginx -T.
6
Lock the winner into systemd and the image, or the next reboot replays the race.

Common mistakes to avoid

5 patterns
×

Looping systemctl restart without naming the port owner

Symptom
Ten restarts fail identically while the real owner keeps serving; the incident log fills with noise instead of one PID.
Fix
Run ss -ltnp once and record the PID before any restart. Restarts can't evict another process.
×

Using kill -9 on the Nginx master first

Symptom
Orphaned workers keep the sockets open, so the next start fails again — now with no master to signal cleanly.
Fix
Drain with nginx -s quit, escalate to TERM only if it hangs, and reserve KILL for true wedges.
×

Editing a site file Nginx never loads

Symptom
The fix is on disk but 502s or bind errors persist because the live config comes from a different sites-enabled symlink.
Fix
Verify with nginx -T that your file appears in the merged config before reloading.
×

Treating errno 13 like errno 98

Symptom
Hours hunting a port squatter while the log says Permission denied — the port is free, the user just can't take it.
Fix
Read the errno first: 98 hunts processes, 13 fixes capabilities or the listen user.
×

Fixing the clash but not the boot order

Symptom
The box works until the next reboot, when Apache and Nginx race again and the outage replays itself.
Fix
Disable the loser with systemctl disable and encode the winner in the image build.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Nginx fails with bind() (98: Address already in use). What's your first ...
Q02JUNIOR
nginx -t passes but Nginx won't start with a bind error. Why isn't that ...
Q03SENIOR
ss shows nginx itself holding port 80, but systemctl says Nginx is down....
Q04SENIOR
How do errno 98 and errno 13 in the bind line change your response?
Q05SENIOR
Design a deploy pipeline that makes this outage impossible.
Q01 of 05JUNIOR

Nginx fails with bind() (98: Address already in use). What's your first command?

ANSWER
sudo ss -ltnp 'sport = :80' to name the PID and process holding the port. The errno says a socket is owned; only ss names the owner, and everything after depends on whether it's a stale Nginx, Apache, or a container proxy.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I just reboot the server to clear it?
02
Why does nginx -t pass when Nginx can't start?
03
Is it safe to kill -9 the stale Nginx master?
04
Can Nginx and Apache share port 80?
05
Why does the error mention IPv6 [::]:80 separately?
06
How do I stop Docker and host Nginx fighting over 80?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Networking. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Too Many Open Files Fix
5 / 5 · Networking
Next
Docker Exec Format Error Fix
→