You deploy a service, it starts to bind, and the kernel replies with the most unhelpful truth in computing: bind: Address already in use. The pager goes off. Someone says “just reboot it.” Someone else says “it’s probably DNS.” Neither is your friend right now.
On Debian 13, you have all the tools you need to identify exactly who owns a port—process, systemd unit, container, socket activation, even a stale namespace—and then fix the conflict without collateral damage. The trick is knowing the order to look, and what each tool is actually telling you.
Fast diagnosis playbook
This is the “stop bleeding” sequence. It’s optimized for finding the owner fast, not for teaching. Teaching comes later.
First: confirm what’s actually failing (service logs)
Don’t start with port scanners. Start with the service manager. If systemd is involved, it will often tell you the exact bind address and port that failed.
Second: identify the listener (ss)
Use ss first. It’s modern, fast, and shows the kernel’s view of sockets with process association.
Third: map the process to the real owner (systemd unit / container)
PID ownership is not the same as “who caused this in production.” You want the unit, package, or container image behind it.
Fourth: check for systemd socket activation and port-forwarders
The port may be held by a socket unit, not the daemon you think. Or by a proxy (Envoy, HAProxy, nginx) that someone forgot to mention.
Fifth: choose a clean fix
Prefer disabling the wrong listener, correcting configuration, or migrating the service to a dedicated port. Avoid “kill -9” unless you enjoy postmortems.
Interesting facts and context (why this keeps happening)
- Fact 1: The error string comes from
EADDRINUSE, a POSIX errno used bybind(2)when a local address/port tuple is already reserved. - Fact 2: Before
ssbecame common, Linux admins leaned onnetstat(from net-tools). Debian has been nudging people away from net-tools for years, and Debian 13 is firmly in the “use iproute2” era. - Fact 3: Systemd socket activation can bind ports before the service starts. That means the port owner might be systemd, not your daemon.
- Fact 4: IPv6 can “cover” IPv4 via v6-mapped sockets depending on
net.ipv6.bindv6only. You think you’re binding IPv4, but an IPv6 listener is squatting on the port. - Fact 5: Privileged ports (below 1024) historically required root on Unix. Modern Linux can grant that capability to a binary via file capabilities, which changes who can bind what—and who can conflict.
- Fact 6: A port conflict can be caused by an entirely different network namespace (container, systemd-nspawn). Tools on the host may not see it unless you look in the right namespace.
- Fact 7:
SO_REUSEADDRis widely misunderstood. It does not mean “two different programs can listen on the same TCP port.” That’sSO_REUSEPORT, and it has sharp edges. - Fact 8: UDP behaves differently. Multiple UDP sockets can sometimes bind the same port depending on options and addresses, which can make “who owns it” less obvious.
- Fact 9: “Nothing is listening but bind fails” sometimes means your app is trying to bind to an IP that’s still on the box but not on the interface you think (or is managed by VRRP, keepalived, or a cloud agent).
What “Address already in use” really means on Linux
When a server starts, it usually does some version of: create a socket, set options, then bind() to a local address/port, then listen(). If the kernel can’t reserve that address/port combination, it returns EADDRINUSE. Your program prints the error and exits (or retries if it’s polite).
Here’s the important part: “Address” includes more than a port number. It can mean:
- TCP vs UDP: TCP port 53 and UDP port 53 are different sockets. One can be in use while the other is free.
- IP-specific vs wildcard: Binding to
127.0.0.1:8080is different from binding to0.0.0.0:8080(all IPv4). But a wildcard bind can block a specific bind, depending on how it’s already bound. - IPv6 wildcard:
[::]:8080can, on some systems, also accept IPv4 connections unless the kernel is configured as v6-only. - Network namespaces: If you’re inside a container namespace, “port 8080” is not necessarily the host’s port 8080—unless you published it.
If you take only one lesson from this section: always capture the full bind target from logs—protocol, IP, and port—before you chase ghosts.
One paraphrased idea from Richard Cook (reliability researcher): Failures happen in the gaps between how work is imagined and how it’s actually done.
Port ownership is one of those gaps.
Practical tasks: commands, outputs, decisions (12+)
These are the tasks you run on a Debian 13 host when a service can’t start because “Address already in use.” Each one includes what the output means and what decision you make next.
Task 1: Read the failing unit’s logs (systemd)
cr0x@server:~$ sudo systemctl status myapp.service
× myapp.service - MyApp API
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Mon 2025-12-29 09:12:01 UTC; 8s ago
Process: 18422 ExecStart=/usr/local/bin/myapp --listen 0.0.0.0:8080 (code=exited, status=1/FAILURE)
Main PID: 18422 (code=exited, status=1/FAILURE)
CPU: 48ms
Dec 29 09:12:01 server myapp[18422]: bind: Address already in use
Dec 29 09:12:01 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
Dec 29 09:12:01 server systemd[1]: myapp.service: Failed with result 'exit-code'.
Meaning: You got the bind target: 0.0.0.0:8080. That’s the full IPv4 wildcard port. Now you can search precisely.
Decision: Go find who is already listening on TCP/8080, on IPv4 or IPv6 wildcard.
Task 2: Find listeners with ss (fast, accurate)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("nginx",pid=1312,fd=8))
Meaning: TCP port 8080 is already bound on all IPv4 addresses by nginx PID 1312.
Decision: Determine whether nginx is supposed to own 8080. If not, adjust nginx config or stop/disable the unit.
Task 3: Confirm IPv6 isn’t also involved
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080' -6
LISTEN 0 4096 [::]:8080 [::]:* users:(("nginx",pid=1312,fd=9))
Meaning: nginx is also listening on IPv6 wildcard. Even if you fix IPv4, IPv6 may still collide depending on your app’s bind behavior.
Decision: Decide whether your new service should listen on IPv6 too, and ensure nginx is moved off the port entirely, not “half fixed.”
Task 4: Translate PID to systemd unit (the real owner)
cr0x@server:~$ ps -o pid,comm,args -p 1312
PID COMMAND COMMAND
1312 nginx nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
cr0x@server:~$ systemctl status nginx.service
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2025-12-29 08:55:10 UTC; 17min ago
Main PID: 1312 (nginx)
Tasks: 2 (limit: 18920)
Memory: 4.9M
CPU: 1.205s
Meaning: It’s a managed service, not a random stray process. Great: you can fix this cleanly.
Decision: Check nginx config for a listener on 8080, then change it or remove it, then reload.
Task 5: Locate the binding directive in nginx
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'listen\s+8080'
47: listen 8080 default_server;
98: listen [::]:8080 default_server;
Meaning: nginx is explicitly configured to use 8080 on both stacks.
Decision: Either change nginx to another port, or reassign your app to a different port, or put your app behind nginx and keep 8080 for nginx. Pick one, don’t “share.”
Task 6: If it’s not a managed service, identify the package or parent
cr0x@server:~$ sudo ss -H -ltnp 'sport = :5432'
LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=2221,fd=6))
cr0x@server:~$ ps -fp 2221
UID PID PPID C STIME TTY TIME CMD
postgres 2221 1 0 08:41 ? 00:00:01 /usr/lib/postgresql/17/bin/postgres -D /var/lib/postgresql/17/main
Meaning: Postgres is listening on localhost only, still occupying 5432.
Decision: If you tried to start another Postgres or an app that wants 5432, you either stop the existing instance or move one of them. Starting “another Postgres on 5432” is not a plan.
Task 7: Use lsof when ss doesn’t show what you expect
cr0x@server:~$ sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1312 root 8u IPv4 24562 0t0 TCP *:8080 (LISTEN)
nginx 1312 root 9u IPv6 24563 0t0 TCP *:8080 (LISTEN)
Meaning: Same story, different tool. lsof is slower but sometimes clearer, especially under pressure.
Decision: If tools disagree, trust the kernel view but verify namespace (see later). Most of the time, disagreement means you’re looking in different namespaces or you filtered incorrectly.
Task 8: Check for systemd socket activation holding the port
cr0x@server:~$ systemctl list-sockets --all | grep -E ':(80|443|8080)\b'
nginx.socket 0 128 0.0.0.0:8080 0.0.0.0:*
nginx.socket 0 128 [::]:8080 [::]:*
Meaning: The port might be owned by a socket unit, which can keep listening even if the service is stopped. This is a classic “I stopped it, why is it still in use?” scenario.
Decision: Manage the .socket unit, not just the .service. Disable/mask the socket if you don’t want systemd to own the port.
Task 9: Stop and disable the correct unit (service vs socket)
cr0x@server:~$ sudo systemctl stop nginx.service
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("systemd",pid=1,fd=67))
cr0x@server:~$ sudo systemctl stop nginx.socket
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
cr0x@server:~$ sudo systemctl disable --now nginx.socket
Removed "/etc/systemd/system/sockets.target.wants/nginx.socket".
Meaning: Stopping the service did nothing because systemd kept the socket. Stopping the socket freed the port.
Decision: If nginx is not supposed to manage that port anymore, disable the socket (and ensure the service isn’t enabled unintentionally either).
Joke 1: If you “fix” a port conflict by rebooting, you didn’t solve it—you just power-cycled the mystery.
Task 10: Confirm which IP address your app is binding to
cr0x@server:~$ ip -br addr show
lo UNKNOWN 127.0.0.1/8 ::1/128
ens3 UP 10.10.5.21/24 2001:db8:10:10::21/64
Meaning: You can only bind to addresses you own (or will own via a manager like keepalived). If your app tries to bind to 10.10.5.99 and you don’t have it, you’ll get a different error (usually Cannot assign requested address), but people frequently misread logs and chase the wrong issue.
Decision: Validate the bind address matches reality. If it’s a VIP, confirm the VIP is present on this node.
Task 11: Catch a “hidden” listener in another network namespace (containers)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :9090'
LISTEN 0 4096 0.0.0.0:9090 0.0.0.0:* users:(("docker-proxy",pid=5144,fd=4))
Meaning: The host port is held by docker-proxy (or sometimes by nftables rules without docker-proxy, depending on setup). Your real application is inside a container, but the host-side bind is what blocks your service.
Decision: Find which container published the port. Don’t kill docker-proxy; fix the container mapping.
Task 12: Identify the container that published the port (Docker)
cr0x@server:~$ sudo docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Ports}}'
CONTAINER ID NAMES PORTS
c2f1d3a7b9c1 prometheus 0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp
8bca0e23ab77 node-exporter 9100/tcp
cr0x@server:~$ sudo docker inspect -f '{{.Name}} {{json .HostConfig.PortBindings}}' c2f1d3a7b9c1
/prometheus {"9090/tcp":[{"HostIp":"","HostPort":"9090"}]}
Meaning: The prometheus container owns host 9090.
Decision: If your new service needs 9090, move Prometheus (recommended: keep 9090 for Prometheus and move your service), or change the published port and update scrape configs.
Task 13: Identify listeners created by a user session (not root, not systemd)
cr0x@server:~$ sudo ss -H -ltnp 'sport = :3000'
LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=27533,fd=21))
cr0x@server:~$ ps -o pid,user,cmd -p 27533
PID USER CMD
27533 alice node /home/alice/dev/server.js
cr0x@server:~$ sudo loginctl session-status
2 - alice (1000)
Since: Mon 2025-12-29 07:58:02 UTC; 1h 17min ago
Leader: 27102 (sshd)
Seat: n/a
TTY: pts/1
Service: sshd
State: active
Unit: session-2.scope
└─27533 node /home/alice/dev/server.js
Meaning: A human is running a dev server on production. This happens more than anyone admits.
Decision: Coordinate. Either move the dev process, or reserve the port for production and tell people to use a non-conflicting port bound to localhost (or better: a dev host).
Task 14: Handle UDP conflicts (DNS, metrics, game servers)
cr0x@server:~$ sudo ss -H -lunp 'sport = :53'
UNCONN 0 0 127.0.0.1:53 0.0.0.0:* users:(("systemd-resolve",pid=812,fd=13))
Meaning: UDP/53 is already bound on localhost by a resolver. If you’re trying to start a DNS server, you’ll conflict.
Decision: Decide whether this host should run a DNS server. If yes, you may need to reconfigure the local resolver service to stop binding, or bind your DNS service to a specific interface/IP that doesn’t conflict (careful).
Task 15: Verify a service’s configured Listen* and ports via systemd
cr0x@server:~$ systemctl cat nginx.socket
# /lib/systemd/system/nginx.socket
[Unit]
Description=nginx Socket
[Socket]
ListenStream=8080
ListenStream=[::]:8080
Accept=no
[Install]
WantedBy=sockets.target
Meaning: The socket unit is the bind source. This is the smoking gun when a port “keeps coming back.”
Decision: Change/override this unit, or disable/mask it.
Task 16: Confirm the port is actually free after a change
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
cr0x@server:~$ sudo systemctl start myapp.service
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080'
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("myapp",pid=19201,fd=7))
Meaning: Ownership is clean: the expected process is now listening.
Decision: Move on to health checks, routing, and whatever else the outage gods have queued next.
Systemd gotchas: sockets, units, lingering listeners
On Debian 13, systemd is the center of gravity. Even if you think you’re “just running a binary,” you’re probably doing it via a unit file, a timer, a socket, or a container service.
Socket activation is great—until you forget it exists
Socket activation means systemd binds the port, then launches the service on demand and hands it the file descriptor. It improves startup latency for request-driven services, and it can smooth restarts. It also creates a failure mode: you stop a service, but the port stays bound by PID 1.
If you see users:(("systemd",pid=1,...)) in ss output for your port, don’t argue with it. Go find the socket unit:
cr0x@server:~$ systemctl list-sockets --all | grep ':8080'
nginx.socket 0 128 0.0.0.0:8080 0.0.0.0:*
nginx.socket 0 128 [::]:8080 [::]:*
Masking: the nuclear option that’s sometimes correct
Disabling a unit prevents it from starting at boot, but something else can still start it manually or as a dependency. Masking blocks it completely by linking it to /dev/null. In incident response, masking can be the right move when an unwanted listener keeps resurrecting.
cr0x@server:~$ sudo systemctl mask --now nginx.socket
Created symlink /etc/systemd/system/nginx.socket → /dev/null.
Decision rule: Disable when you’re changing intended behavior and you’ve got time to do it properly. Mask when you need to guarantee it won’t come back (and you’ll clean up afterward).
Drop-ins beat editing vendor unit files
On Debian, vendor units live under /lib/systemd/system. Editing them works until the next package upgrade, when your fix gets politely removed.
Create an override instead:
cr0x@server:~$ sudo systemctl edit nginx.socket
[Socket]
ListenStream=
ListenStream=127.0.0.1:8080
Meaning: The empty ListenStream= line resets the list, then you set the new one. This is systemd’s way of making sure you really meant “replace,” not “append.”
Decision: Use drop-ins for stable behavior across upgrades. Editing vendor units is a temporary hack you should treat like a temporary hack.
Containers and orchestration: Docker, Podman, Kubernetes
Port conflicts in containerized environments are rarely “two daemons both want 8080.” They’re usually “something published 8080 on the host months ago and everyone forgot.” Containers make it easy to ship software; they also make it easy to squat on ports indefinitely.
Docker and host port publishing
When you run -p 8080:8080, you are explicitly claiming the host port. On many setups you’ll see docker-proxy owning the socket. On others, nftables rules handle forwarding, but the host port can still appear reserved depending on mode.
The clean workflow is:
- Identify that a proxy is the listener (
ssshows docker-proxy). - Map it to a container (
docker ps,docker inspect). - Change the published port or remove the container.
Podman: same idea, different plumbing
Podman can run rootless containers. Rootless port binding introduces a twist: processes may live under a user session, and port forwarding can be done by slirp4netns or pasta. The symptom is still the same: your host port is taken.
If you suspect Podman:
cr0x@server:~$ podman ps --format 'table {{.ID}}\t{{.Names}}\t{{.Ports}}'
CONTAINER ID NAMES PORTS
4fdb08b1b3a8 grafana 0.0.0.0:3000->3000/tcp
Decision: If it’s rootless and tied to a user, you may need to coordinate with that user session or disable lingering for that account.
Kubernetes: NodePort and hostNetwork
Kubernetes adds a special kind of chaos:
- NodePort grabs ports in a range, and you can collide if something else listens there.
- hostNetwork: true makes pods bind directly on the node’s network stack.
- DaemonSets mean “it’s everywhere,” which is great when intentional and extremely annoying when not.
On a node with kubelet, ss will show the owning process (sometimes the application process itself, sometimes a proxy). Your job is to map it to the pod and workload. The exact commands depend on your cluster access, but the host-side diagnosis doesn’t: identify the listener first.
IPv4/IPv6 and “it’s listening, but not where you looked”
The classic confusion: you run ss -ltnp, you don’t see your port, but the app still fails with “Address already in use.” Then you run it again and see something on [::]. You say “we don’t use IPv6.” The kernel does not care about your beliefs.
Understand the wildcard listeners
These are the patterns you should recognize:
0.0.0.0:PORTmeans all IPv4 addresses.[::]:PORTmeans all IPv6 addresses. Depending on sysctl, it may also accept IPv4-mapped connections.127.0.0.1:PORTmeans localhost only (usually safest for internal admin endpoints).
Check bindv6only when behavior is surprising
cr0x@server:~$ sysctl net.ipv6.bindv6only
net.ipv6.bindv6only = 0
Meaning: With 0, an IPv6 wildcard socket can also accept IPv4 connections via v4-mapped addresses on many systems. That can make [::]:8080 block 0.0.0.0:8080 expectations depending on how applications set options.
Decision: Don’t “fix” this by flipping sysctls during an incident unless you understand the blast radius. Fix it at the application/service config level: bind explicitly to IPv4 or IPv6 as intended.
Races, restarts, and TIME_WAIT myths
People blame TIME_WAIT for port binding failures the way they blame “the network” for slow queries. It’s sometimes involved, but rarely the cause of a server not being able to bind its listening port.
TIME_WAIT is usually about outbound connections
TIME_WAIT sockets are typically client-side ephemeral ports, not server listening ports. Your server can usually rebind its listening port just fine. If it can’t, you almost certainly have a real listener, a socket unit, or a process stuck.
When restarts cause bind conflicts anyway
The real restart problem is two instances overlap:
- systemd starts a new instance before the old one truly exits (misconfigured
Type=forking, incorrect PID tracking). - An app daemonizes but the unit is written for a non-daemon process, so systemd thinks it died and restarts it—creating multiple attempts to bind.
- A wrapper script spawns the real server and exits immediately.
To detect this, watch for multiple processes with the same binary, or for rapid restart loops in the journal.
cr0x@server:~$ sudo journalctl -u myapp.service -n 50 --no-pager
Dec 29 09:11:58 server systemd[1]: Started MyApp API.
Dec 29 09:12:01 server myapp[18422]: bind: Address already in use
Dec 29 09:12:01 server systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
Dec 29 09:12:01 server systemd[1]: myapp.service: Failed with result 'exit-code'.
Dec 29 09:12:01 server systemd[1]: myapp.service: Scheduled restart job, restart counter is at 5.
Dec 29 09:12:01 server systemd[1]: Stopped MyApp API.
Dec 29 09:12:01 server systemd[1]: Started MyApp API.
Decision: If there’s a restart loop, stop the unit to stabilize the host, then fix the conflict. Otherwise you’ll be chasing a moving target while systemd repeatedly spawns failures.
Clean fixes you can defend in a change review
You can “free the port” in a lot of ways. Most of them are lazy. The goal is to fix the ownership model so the conflict doesn’t come back on the next reboot, redeploy, or well-meaning engineer’s Friday afternoon.
Fix option A: Move the service to a correct port (and document it)
If two services both want 8080 because nobody picked a port plan, pick one now. Put it in config management. Put it in monitoring labels. Make it boring.
Fix option B: Put one service behind a reverse proxy
If nginx is already on 80/443 and your app wants 8080, the clean architecture is usually: nginx owns the public ports; apps live on private high ports or unix sockets; nginx routes.
Be consistent. “Sometimes we bind apps directly to 0.0.0.0” is how you get surprise port wars.
Fix option C: Stop/disable the wrong service (service + socket if needed)
If the port is owned by something you don’t want, remove it from the boot graph:
cr0x@server:~$ sudo systemctl disable --now nginx.service
Removed "/etc/systemd/system/multi-user.target.wants/nginx.service".
cr0x@server:~$ sudo systemctl disable --now nginx.socket
Removed "/etc/systemd/system/sockets.target.wants/nginx.socket".
Decision: Disabling both prevents the “I stopped it but it came back” dance.
Fix option D: Make binds explicit (avoid wildcard when you don’t need it)
Binding to 0.0.0.0 is convenient and frequently wrong. For admin endpoints, bind to localhost. For internal services, bind to the internal interface IP or a dedicated VRF. For public services, bind to the load balancer VIP or let the proxy own it.
Fix option E: Use file capabilities for privileged ports instead of running as root
If your service needs 80/443 but shouldn’t run as root, set capabilities:
cr0x@server:~$ sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/myapp
cr0x@server:~$ getcap /usr/local/bin/myapp
/usr/local/bin/myapp cap_net_bind_service=ep
Meaning: The binary can bind privileged ports without full root privileges.
Decision: Use this when you need it, but track it—capabilities are easy to forget and they change your security posture. Also: capabilities don’t prevent conflicts; they just change who can create them.
Fix option F: Reserve ports and enforce policy
In serious environments, you maintain a port registry for each host role or cluster. You enforce it in CI, in helm charts, in systemd templates. Without that, port conflicts aren’t “incidents,” they’re “inevitable.”
Joke 2: Port allocation without a registry is like seating at a conference lunch—everyone ends up fighting over the one table near the power outlet.
Common mistakes: symptom → root cause → fix
1) “I stopped the service but the port is still in use”
Symptom: systemctl stop foo.service succeeds, but ss still shows the port in LISTEN.
Root cause: A foo.socket unit is binding the port (socket activation), or another dependent service still holds it.
Fix: Check systemctl list-sockets. Stop/disable/mask the socket unit. Verify with ss that PID 1 is no longer the listener.
2) “ss shows nothing, but my app still says Address already in use”
Symptom: You search with ss and see no listener on that port.
Root cause: You searched the wrong protocol (UDP vs TCP), wrong address family (IPv4 vs IPv6), or your filters excluded it. Less common: you’re in a different network namespace than the listener.
Fix: Search both stacks and protocols: ss -ltnp, ss -lunp, include -6. If containers are involved, check for docker-proxy and inspect container mappings.
3) “The port is owned by ‘systemd’ and I don’t know why”
Symptom: ss shows users:(("systemd",pid=1,...)).
Root cause: A socket unit is configured to bind it (sometimes under a name you didn’t expect, or pulled in by a package).
Fix: Identify the socket via systemctl list-sockets --all, then systemctl cat NAME.socket. Disable or override its ListenStream/ListenDatagram.
4) “It works on IPv4 but not on IPv6” (or vice versa)
Symptom: The service starts when bound to 127.0.0.1 but fails on 0.0.0.0, or it fails only when configured for [::].
Root cause: Another service is bound on one family; or v6 dual-stack behavior is blocking v4.
Fix: Inspect listeners for both families. Make binds explicit in configs. Don’t rely on defaults.
5) “We changed the port, but it keeps reverting”
Symptom: After editing a unit or config, the old port returns after upgrades/reboots.
Root cause: You edited vendor config under /lib, or a config management tool overwrote your change.
Fix: Use systemd drop-ins under /etc/systemd/system. If config management exists, make it the source of truth.
6) “Killing the PID fixed it, but now other things are broken”
Symptom: You used kill -9 on the listener; the port freed; later you discover side effects.
Root cause: You killed a shared component (proxy, ingress, metrics agent) or a supervised service that automatically restarted into the same port again.
Fix: Map PID → unit/container before killing. Stop it via systemd/Docker so it stays stopped (or change its config). Use kill only when you’ve decided the blast radius is acceptable.
Checklists / step-by-step plan
Checklist: Identify the owner in under 2 minutes
- Read the exact bind target from logs (
systemctl statusor app logs). Capture protocol, IP, and port. - Run
ss -ltnp 'sport = :PORT'for TCP;ss -lunp 'sport = :PORT'for UDP. - If nothing shows, check IPv6 explicitly with
-6and ensure you didn’t typo the port. - Once you have PID/command, map to systemd unit via
systemctl statusandps. - If the listener is systemd PID 1, list sockets and find the
.socketunit. - If the listener is docker-proxy/container plumbing, identify the container publishing the port.
Checklist: Make the fix durable
- Decide which service should own the port (architecture decision, not a coin flip).
- Implement config changes using drop-ins or managed config, not vendor file edits.
- Stop/disable the old owner (service and socket if relevant).
- Start the intended owner and verify with
ss. - Run a local connectivity test (curl, nc) and check external routing if applicable.
- Add a monitoring check that detects unexpected listeners on critical ports.
Checklist: Safe “emergency free port” procedure
- Stop the failing service to avoid restart loops.
- Identify the current owner with
ss/lsof. - Stop it cleanly using its supervisor (systemd/Docker) rather than killing.
- Only if clean stop fails: send SIGTERM, wait, then consider SIGKILL.
- Document what you did and why. Future-you will not remember at 3 a.m.
Three corporate-world mini-stories
Mini-story 1: The incident caused by a wrong assumption
The team had a simple rollout: deploy a new internal API on port 8080, behind an existing reverse proxy. Everyone “knew” 8080 was the internal app port. It was in someone’s head and in a two-year-old wiki page nobody trusted enough to update.
The deployment failed with “Address already in use.” The on-call engineer did the standard ss check and saw nginx listening on 8080. That seemed impossible—nginx “owned” 80 and 443. So they assumed ss was showing a stale artifact, and restarted the box. (It came back exactly the same, because of course it did.)
Once people stopped rebooting things as a form of inquiry, the root cause was embarrassingly mundane: a previous migration had moved an old legacy app from 80 to 8080 temporarily, and nginx was left listening on 8080 as a redirector. It was a “temporary” that lasted three quarters.
The fix wasn’t “kill nginx.” The fix was a small architectural decision: nginx gets public ports; internal apps get an allocated high port per service; 8080 is not “default,” it’s “allocated.” They moved the legacy redirector back under 80/443 and freed 8080 (or more accurately, they stopped using “8080” as a magical belief system).
The post-incident action that actually mattered: a port registry tied to service ownership. Not a spreadsheet—something enforced in config review. The error stopped being mysterious because ownership stopped being tribal knowledge.
Mini-story 2: The optimization that backfired
A performance-minded engineer wanted faster restarts during deploys. They enabled systemd socket activation for a small HTTP service so that systemd would hold the socket and hand it off to a new process with less downtime. Smart idea, used in the right places.
Then they forgot they’d done it. Months later, another team tried to deploy a separate service on the same port during a consolidation. They stopped the old service, saw it was inactive, and tried to start the new one. It failed with “Address already in use.” ss showed PID 1 holding the port. Confusion escalated quickly.
In the corporate reality, the incident wasn’t just technical. A change request went in to “kill the systemd process holding 8443.” That’s how you know you’re in trouble: when someone proposes killing init because of a port conflict.
The actual fix was clean: stop and disable the .socket unit, then start the new service. The lesson was sharper: socket activation is a deployment feature, not a set-and-forget toggle. If you turn it on, you own the operational complexity it adds, including how it changes “what it means” for a service to be stopped.
Afterward they codified a rule: socket-activated services must be documented in the unit description and monitored with a “socket bound while service stopped” alert. That’s the boring guardrail that prevents “optimization” from turning into “mystery.”
Mini-story 3: The boring but correct practice that saved the day
A storage-adjacent service ran on a Debian fleet that also hosted metrics, log shippers, and a couple of legacy daemons nobody dared remove. Ports were a minefield. But this team had one dull practice: before any rollout, they ran a preflight script that checked a small list of reserved ports and verified the expected owner.
During a routine patch window, a new metrics container was deployed with a default -p 9100:9100 because the chart author assumed Node Exporter was always “inside the cluster.” On these hosts, Node Exporter was already running as a systemd service. The container grabbed the port first on a subset of nodes due to scheduling order. The rollout looked “mostly fine,” which is a great way to get hurt.
The preflight caught it on the first node in the batch: the script saw that 9100 was no longer owned by the expected unit. The deploy paused, not after it broke half the fleet—after it threatened one host.
The fix took minutes: remove the host port publishing from the container, use the existing Node Exporter, and keep the chart from claiming the port again. No drama, no rebooting, no war room. Just a small boring check that prevented a large boring outage.
This is what “operational excellence” looks like in practice: it’s not heroics, it’s refusing to let surprises into production.
FAQ
1) Why does “Address already in use” happen when nothing is running?
Usually because something is running, just not the thing you expect: a systemd socket unit, a container proxy, or a listener bound on IPv6 while you searched IPv4. Verify with ss across families and check systemctl list-sockets.
2) What’s the best command to find who is using a port on Debian 13?
ss -ltnp (TCP) and ss -lunp (UDP). Use a precise filter like 'sport = :8080' so you don’t drown in output.
3) Should I install net-tools for netstat?
No, unless you’re dealing with muscle memory during an incident and you accept the tradeoff. Debian 13 is built for iproute2 tooling. Learn ss; it’s worth it.
4) Can two processes listen on the same TCP port?
Not in the simple way people hope. There are advanced cases with SO_REUSEPORT where multiple workers share a port, but that’s typically within one service design. For two unrelated daemons, treat it as “no.”
5) Does TIME_WAIT prevent my service from binding its port?
Almost never for the server listening port. TIME_WAIT is more commonly about closed outbound connections. If bind fails, find the actual listener or socket unit.
6) Why does ss show systemd owns the port?
Socket activation. systemd is intentionally binding the port via a .socket unit. Stop/disable/mask the socket unit if you want the port freed permanently.
7) How do I fix a port conflict without rebooting?
Identify the owner, stop it via its supervisor (systemd/Docker), disable the auto-start path (service/socket/container), then start the intended service and verify with ss.
8) How do I tell whether the conflict is IPv4 or IPv6?
Check both: ss -ltnp 'sport = :PORT' and ss -ltnp -6 'sport = :PORT'. Look for 0.0.0.0 vs [::], and for specific addresses like 127.0.0.1.
9) What if the port is owned by a user’s process and I need it back?
Don’t start with killing. Identify the user and session, coordinate, and set policy: production ports are reserved. If you must reclaim it quickly, stop the process with SIGTERM, then follow up with a durable fix.
Conclusion: next steps that won’t wake you up again
“Address already in use” is not a puzzle. It’s an ownership dispute. The kernel is telling you there’s already a tenant on that address/port tuple, and your job is to figure out whether that tenant is legitimate.
Do this next:
- Standardize on
ssfor port ownership checks and make it part of your runbook. - When you find a PID, always map it to a unit or container. Fix the system that starts it, not just the process.
- Audit for socket-activated services and document them. If PID 1 owns a port, it’s probably intentional.
- Create a port allocation registry for your environment (even a small one). Enforce it in reviews.
- Add a lightweight check that validates critical ports are owned by expected services, before deployments proceed.
Reboots are for kernel upgrades and the occasional driver tantrum. Port conflicts deserve better: evidence, a clean fix, and a future where you don’t have to rediscover the same truth at 2 a.m.