How to Fix Docker Error: listen tcp4 0.0.0.0:80 bind address already in use
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: Docker's userland proxy cannot bind to
0.0.0.0:80because a host process (nginx, Apache, another container, or systemd socket) already owns that TCP port. - How to fix it: Kill or reconfigure the conflicting process, or remap your Docker container to a free port.
- Use our Client-Side Sandbox above to paste your
docker-compose.ymland auto-generate the corrected port binding configuration.
The Incident (What Does the Error Mean?)
Raw error output from Docker daemon:
Error response from daemon: driver failed programming external connectivity on endpoint my_container:
Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use
Docker's userland proxy (docker-proxy) is a small Go process that forwards traffic from the host port to the container's internal port. When it calls bind() on 0.0.0.0:80, the kernel returns EADDRINUSE. The container never starts. Any dependent services in the same compose stack that rely on this container will also fail to initialize.
The Attack Vector / Blast Radius
This is a full service outage vector, not merely an inconvenience:
- Production impact: If this fires during a rolling deploy or a container restart, your service is down. Load balancers start returning 502/503 immediately.
- Silent conflict source: On most Ubuntu/Debian servers,
apache2ornginxis installed as a system package and starts on boot via systemd — before Docker even initializes. Engineers forget this constantly. - Cascading failure in Compose stacks: A
depends_onchain means every downstream container (app servers, workers) that needs the proxy container will also refuse to start. One port conflict can take down an entire stack. - CI/CD pipeline breakage: If your integration test runner spins up Docker containers on a shared build agent, a stale container or a host-level service occupying port 80 will cause every pipeline run to fail non-deterministically — the hardest class of bug to diagnose.
--net=hostmisuse risk: Engineers under pressure often "fix" this by switching to--net=host, which eliminates network isolation entirely and exposes all container ports directly on the host. This is a security regression.
How to Fix It
Step 1: Identify the Offending Process
# Most direct — shows PID and process name holding port 80
sudo ss -tlnp | grep ':80'
# Alternative
sudo lsof -i tcp:80
# If it's another Docker container
docker ps --format '{{.Names}}\t{{.Ports}}' | grep '0.0.0.0:80'
Common offenders:
| Process | How to stop it |
|---|---|
nginx (host) |
sudo systemctl stop nginx |
apache2 (host) |
sudo systemctl stop apache2 |
| Stale Docker container | docker rm -f <container_id> |
systemd-resolved |
Reconfigure to not bind port 80 |
Basic Fix — Remap the Host Port
If you cannot or should not stop the conflicting service, remap Docker to a free port and put a reverse proxy in front.
# docker-compose.yml
services:
web:
image: nginx:alpine
ports:
- - "80:80"
+ - "8080:80"
Then point your host-level nginx or HAProxy at localhost:8080.
Enterprise Best Practice — Explicit Port Reservation + Reverse Proxy Pattern
Never bind application containers directly to port 80/443 on the host. Use a dedicated edge proxy container (Traefik, Caddy, or nginx) as the sole owner of host ports 80/443. All app containers communicate over an internal Docker network.
# docker-compose.yml
services:
- web:
- image: my-app:latest
- ports:
- - "80:8000"
+ traefik:
+ image: traefik:v3.0
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - ./traefik.yml:/traefik.yml:ro
+ networks:
+ - edge
+
+ web:
+ image: my-app:latest
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.web.rule=Host(`app.example.com`)"
+ networks:
+ - edge
+ # No ports: block. Container is NOT directly exposed to host.
+
+ networks:
+ edge:
+ driver: bridge
Why this matters: Only one process owns the host socket. All routing decisions happen at the Traefik layer. Adding new services never risks a port conflict.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Pre-flight Port Check in Pipeline
Add this as a step before docker compose up in your CI script:
#!/bin/bash
PORT=80
if ss -tlnp | grep -q ":${PORT} "; then
echo "[FATAL] Port ${PORT} is already bound on this agent. Aborting."
exit 1
fi
2. Randomized Port Mapping for Test Environments
Use ports: ["0:80"] in CI-specific compose overrides. Docker assigns a random ephemeral host port. Your test suite queries docker inspect to find the assigned port dynamically.
# docker-compose.ci.yml (override file)
services:
web:
ports:
- "0:80" # Docker picks a free host port automatically
3. Checkov Policy — Enforce No Direct Port 80/443 Binding
If you use Checkov for IaC scanning, write a custom check:
# checkov/custom_checks/no_direct_port_80.py
from checkov.common.models.enums import CheckResult
from checkov.dockerfile.checks.base_dockerfile_check import BaseDockerfileCheck
class NoDirectPort80Binding(BaseDockerfileCheck):
def __init__(self):
super().__init__(
name="Ensure no service binds directly to host port 80 or 443",
id="CKV_DOCKER_CUSTOM_001",
supported_instructions=["EXPOSE"]
)
def check_resource_attribute(self, attr):
blocked = {"80", "443"}
return CheckResult.FAILED if attr.strip() in blocked else CheckResult.PASSED
4. Disable Host-Level Web Servers on Docker Hosts
# Run once during host provisioning (Ansible, cloud-init, etc.)
sudo systemctl disable --now nginx apache2 2>/dev/null || true
Bake this into your AMI/base image build pipeline so new instances never start with competing services.