Ejecutas docker run -p 8080:80 ... o levantas una pila con Compose, y Docker responde con el equivalente operativo de
“no”: bind: address already in use. De repente estás en respuesta a un incidente un martes porque un puerto está ocupado y nadie
recuerda por qué.
Este es uno de esos errores que parecen sencillos hasta que no lo son. A veces es Nginx. A veces es un contenedor zombie, un socket de systemd,
una limitación de Docker rootless, o el fantasma de un proceso que murió pero dejó un listener atrás. Vamos a encontrar el propietario real, elegir la solución menos mala,
y dejar el host más limpio de lo que lo encontramos.
Qué significa realmente el error (y qué no)
Cuando Docker “publica” un puerto (por ejemplo -p 8080:80), solicita al kernel del host un socket en escucha en el puerto del host
(8080 aquí). El kernel responde o bien “vale” o bien “no”. El “no” que ves suele ser EADDRINUSE: otro socket ya posee ese
4‑tupla exacta (protocolo, dirección local, puerto local).
La clave es que “ya en uso” es más amplio que “otra app está en ejecución”. Puede ser:
- Un proceso distinto escuchando en ese puerto (
nginx,apache,node,sshd, etc.). - Otro contenedor ya publicado en ese puerto del host.
- Un servicio del sistema que se enlaza temprano mediante activación por socket de
systemd. - El propio ayudante de Docker (históricamente
docker-proxy) reteniendo el puerto, incluso si crees que el contenedor se fue. - Un binding a una IP específica, que bloquea un intento posterior de enlazar a
0.0.0.0(o viceversa). - Limitaciones de Docker rootless: enlazar puertos bajos requiere pasos adicionales.
Lo que no es: un problema de firewall. Los firewalls bloquean tráfico; no impiden que se creen sockets en escucha. Si ves “address already in use”,
casi siempre estás buscando un listener, un proxy residual o una colisión de configuración.
Un buen modelo mental: publicar un puerto es una reclamación. Docker quiere reclamar el puerto en el host. El kernel dice que otro ya tiene la escritura.
Tienes que encontrar quién lo posee y decidir si desalojarlo, negociar una nueva dirección o mover tu servicio.
Guía rápida de diagnóstico
Estás bajo presión. Bien. Haz las comprobaciones mínimas en el orden correcto. No empieces a “reiniciar Docker” como si fuera un exorcismo.
Primero: confirma el puerto, protocolo y dirección exactos
- Mira la línea de error de Docker. ¿Es
0.0.0.0:80,127.0.0.1:8080o una IP específica de interfaz? - TCP vs UDP importa. Dos protocolos distintos pueden usar el mismo número de puerto.
Segundo: identifica el listener propietario (el real)
ss -ltnppara TCP,ss -lunppara UDP. Esto suele resolver el misterio.- Si ves
docker-proxy, rastrea el contenedor que lo generó. - Si ves
systemdo una unidad “.socket”, estás en el terreno de la activación por socket.
Tercero: decide la corrección limpia
- Si es un contenedor viejo: detén/elimínalo, y vuelve a ejecutar con el mapeo de puertos previsto.
- Si es un servicio del host: cambia el puerto del servicio host, publica Docker en otro puerto, o pon un proxy inverso adecuado delante.
- Si es activación por socket de
systemd: detén/desactiva la unidad.socket, no solo la unidad.service. - Si es rootless y puertos bajos: usa
setcap, soluciones tipo authbind, o publica puertos altos y ponles un proxy delante.
Cuarto: verifica desde el exterior
curllocalmente, y luego desde otro host si es posible.- Confirma que no “arreglaste” el bind rompiendo el enrutamiento (NAT, iptables/nftables) o la exposición IPv6.
Un chiste corto, porque te lo mereces: Los puertos son como salas de reuniones: todo el mundo las necesita urgentemente, nadie las reservó, y de algún modo Finanzas ya está dentro.
Hechos y contexto histórico (para que dejes de discutir con el kernel)
Estas cosas parecen arbitrarias hasta que conoces un poco de historia y comportamiento del kernel. Aquí hay hechos concretos que importan cuando depuras:
- Los puertos se poseen por protocolo. TCP 443 y UDP 443 son reclamaciones separadas. Un puerto TCP ocupado no bloquea UDP con el mismo número.
- La dirección de bind importa. Un proceso puede enlazarse a
127.0.0.1:8080sin bloquear a otro que enlaza a192.168.1.10:8080, pero un bind a0.0.0.0:8080bloquea todas las direcciones IPv4. - Linux usa
SO_REUSEADDRySO_REUSEPORTde forma distinta. No significan “ignorar conflictos”; controlan la reutilización de direcciones y la semántica de distribución de carga. TIME_WAITno es un socket en escucha. Es un estado de conexión; normalmente no impide un nuevo listener en ese puerto.- Docker históricamente usó un proxy en espacio de usuario (
docker-proxy) para puertos publicados. Dependiendo de la versión y configuración de Docker, aún puedes verlo, y puede retener puertos incluso cuando la red se vuelve extraña. - La transición iptables → nftables cambió los modos de falla. Las distribuciones modernas usan nftables por debajo; mezclar herramientas puede complicar la alcanzabilidad de puertos, aunque no causará
EADDRINUSEpor sí mismo. - systemd socket activation puede enlazar antes de que arranque tu servicio. El listener pertenece a la unidad socket, no al demonio que crees que detuviste.
- Rootless Docker no puede enlazar puertos privilegiados por defecto. El kernel impone privilegios para puertos bajos; existen soluciones, pero deben ser deliberadas.
- IPv6 añade comportamiento paralelo de binds. Un servicio puede enlazar
[::]:80y también cubrir IPv4 vía direcciones mapeadas IPv6 dependiendo denet.ipv6.bindv6only.
Una idea para fiabilidad (parafraseada porque no cito exactamente): parafraseada idea: la esperanza no es una estrategia
— atribuida en salas de operaciones a Edsger W. Dijkstra.
El punto es: prueba qué posee el puerto antes de cambiar nada.
Tareas prácticas: comandos, salidas y decisiones
Estas son tareas prácticas que puedes ejecutar en un host Linux. Cada una incluye: el comando, un fragmento de salida plausible, qué significa y la decisión que tomas.
Hazlas en orden cuando no estés seguro; selecciona según convenga cuando estés confiado.
Task 1: Reproduce el destino de bind exacto que Docker intenta reclamar
cr0x@server:~$ docker run --rm -p 8080:80 nginx:alpine
docker: Error response from daemon: driver failed programming external connectivity on endpoint inspiring_morse (8f4c9b5b1b6a): Bind for 0.0.0.0:8080 failed: port is already allocated.
Significado: Docker intentó enlazar el puerto TCP 8080 en todas las interfaces IPv4 (0.0.0.0) y falló porque el puerto ya está en manos de otro.
Decisión: Deja de adivinar y encuentra el propietario actual de TCP/8080.
Task 2: Identifica el proceso en escucha con ss (rápido, moderno)
cr0x@server:~$ sudo ss -ltnp 'sport = :8080'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=2147,fd=23))
Significado: PID 2147 (node) posee el listener en TCP/8080 en todas las interfaces IPv4.
Decisión: Decide si ese servicio Node debe moverse, detenerse o estar detrás de un proxy inverso mientras Docker usa otro puerto.
Task 3: Muestra la tabla completa de listeners para atrapar suposiciones de “puerto erróneo”
cr0x@server:~$ sudo ss -ltnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=892,fd=3))
LISTEN 0 4096 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=1011,fd=6))
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=2147,fd=23))
LISTEN 0 4096 [::]:80 [::]:* users:(("nginx",pid=1320,fd=7))
Significado: Tienes una mezcla de listeners sólo en loopback y públicos; también observa [::]:80.
Decisión: Si Docker intenta publicar 80, revisa listeners IPv6 también. Si es 8080, el servicio Node es el conflicto.
Task 4: Usa lsof cuando necesites contexto de sistema de ficheros y línea de comandos
cr0x@server:~$ sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 2147 app 23u IPv4 56123 0t0 TCP *:8080 (LISTEN)
Significado: Confirma el listener y la cuenta de usuario, útil para averiguar cómo se inicia (unidad systemd? cron? PM2?).
Decisión: Si es un “servicio misterioso”, ya tienes PID y usuario para trazarlo.
Task 5: Traza el PID hasta una unidad systemd (la pregunta “¿quién inició esto?”)
cr0x@server:~$ ps -p 2147 -o pid,ppid,user,cmd
PID PPID USER CMD
2147 1 app node /opt/api/server.js
cr0x@server:~$ systemctl status 2147
● api.service - Internal API
Loaded: loaded (/etc/systemd/system/api.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2026-01-01 09:13:22 UTC; 1d 2h ago
Main PID: 2147 (node)
Significado: El listener es un servicio gestionado, no un terminal aleatorio.
Decisión: Coordina el cambio: o actualizas api.service para mover puertos, o publicas Docker en otro puerto.
Task 6: Comprueba si Docker ya publicó ese puerto (conflicto entre contenedores)
cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES IMAGE PORTS
web nginx:alpine 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
postgres postgres:16 127.0.0.1:5432->5432/tcp
Significado: Otro contenedor ya posee el puerto del host 8080 y lo mapea al puerto 80 del contenedor.
Decisión: No inicies un segundo servicio en el mismo puerto del host. Elige otro puerto del host, o detén/reemplaza el contenedor existente.
Task 7: Encuentra qué proyecto Compose posee el puerto (cuando los contenedores tienen nombres amables)
cr0x@server:~$ docker inspect -f '{{.Name}} {{range $p, $conf := .NetworkSettings.Ports}}{{$p}}={{(index $conf 0).HostIp}}:{{(index $conf 0).HostPort}} {{end}}' web
/web 80/tcp=0.0.0.0:8080
Significado: Confirma exactamente qué contenedor y mapeo reclamó el puerto del host.
Decisión: Si pretendías un despliegue de reemplazo, haz un intercambio controlado (blue/green o stop-then-start) en lugar de pelear con el kernel.
Task 8: Si docker-proxy está escuchando, mapea de vuelta al contenedor
cr0x@server:~$ sudo ss -ltnp 'sport = :8080'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=3012,fd=4))
cr0x@server:~$ ps -p 3012 -o pid,cmd
PID CMD
3012 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.3 -container-port 80
Significado: El proxy de Docker está reteniendo el puerto del host y reenviando a una IP de contenedor. El contenedor puede existir o no.
Decisión: Comprueba el estado de Docker a continuación; si el contenedor desapareció pero el proxy sigue, trátalo como un bug de Docker/red y limpia de forma segura.
Task 9: Correlaciona IPs de contenedores con un contenedor (cuando sólo tienes 172.17.0.3)
cr0x@server:~$ docker network inspect bridge -f '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'
web 172.17.0.3/16
postgres 172.17.0.4/16
Significado: El proxy está reenviando a web.
Decisión: Si intentas iniciar otro “web”, necesitas un plan de despliegue intencional o un puerto distinto en el host.
Task 10: Si es activación por socket de systemd, detén la unidad socket (no sólo el servicio)
cr0x@server:~$ sudo ss -ltnp 'sport = :80'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("systemd",pid=1,fd=49))
cr0x@server:~$ systemctl list-sockets | grep -E '(:80|http)'
web.socket 0 4096 0.0.0.0:80 0.0.0.0:* LISTENING
cr0x@server:~$ sudo systemctl stop web.socket
cr0x@server:~$ sudo systemctl disable web.socket
Removed "/etc/systemd/system/sockets.target.wants/web.socket".
Significado: PID 1 (systemd) posee el puerto 80 a través de una unidad socket. Detener el servicio no liberará el puerto; detener el socket sí.
Decisión: Desactiva el socket si Docker necesita el puerto. Si el socket es necesario para un demonio del host, no lo enfrentes: publica Docker en otro puerto.
Task 11: Confirma si IPv6 forma parte del conflicto
cr0x@server:~$ sudo ss -ltnp | grep ':80 '
LISTEN 0 4096 [::]:80 [::]:* users:(("nginx",pid=1320,fd=7))
Significado: Algo está escuchando en IPv6 puerto 80. Dependiendo de sysctls y la aplicación, eso puede cubrir también IPv4.
Decisión: Si Docker intenta enlazar 0.0.0.0:80 y falla, detén/traslada el listener IPv6 o enlaza servicios a direcciones distintas explícitamente.
Task 12: Usa curl para confirmar qué está sirviendo actualmente el puerto
cr0x@server:~$ curl -sS -D- http://127.0.0.1:8080/ -o /dev/null
HTTP/1.1 200 OK
Server: Express
Content-Type: text/html; charset=utf-8
Connection: keep-alive
Significado: El puerto 8080 no está “misteriosamente asignado”; está sirviendo activamente una app Express.
Decisión: No vas a liberar este puerto sin afectar a alguien. Planea un movimiento controlado o un proxy inverso.
Task 13: Cuando sospechas un contenedor obsoleto, lista contenedores detenidos y elimina el correcto
cr0x@server:~$ docker ps -a --filter "status=exited" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES STATUS PORTS
web_old Exited (137) 2 hours ago 0.0.0.0:8080->80/tcp
cr0x@server:~$ docker rm web_old
web_old
Significado: Incluso los contenedores finalizados pueden dejar un estado confuso en las personas. El mapeo de puertos debería desaparecer una vez que se elimina el contenedor,
pero verifica con ss.
Decisión: Si el puerto sigue retenido tras la eliminación, estás ante un proxy o problema del demonio: sigue investigando, no reinicies al azar.
Task 14: Verifica que Docker no esté atascado con un proxy huérfano (raro, pero real)
cr0x@server:~$ docker ps --filter "publish=8080"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cr0x@server:~$ sudo ss -ltnp 'sport = :8080'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=3012,fd=4))
Significado: Ningún contenedor en ejecución reclama 8080, pero docker-proxy aún lo hace. Eso es un estado inconsistente.
Decisión: Prefiere una solución dirigida: reinicia Docker para reconciliar los reenvíos de puertos, pero hazlo de forma intencional (drena tráfico, avisa a interesados, conoce el radio de impacto).
Task 15: Reinicia Docker de forma segura (cuando sea necesario)
cr0x@server:~$ sudo systemctl status docker --no-pager
● docker.service - Docker Application Container Engine
Active: active (running) since Thu 2026-01-01 08:11:09 UTC; 1d 3h ago
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ sudo ss -ltnp 'sport = :8080'
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
Significado: El listener desapareció, lo que implica que el proxy estaba huérfano y un reinicio del demonio lo limpió.
Decisión: Repliega inmediatamente el contenedor con el mapeo de puertos previsto y valida las rutas de tráfico. Además: abre un ticket para entender por qué Docker derivó.
Task 16: Diagnostica fallos de bind en Docker rootless (parece similar, causa distinta)
cr0x@server:~$ docker context show
rootless
cr0x@server:~$ docker run --rm -p 80:8080 nginx:alpine
docker: Error response from daemon: driver failed programming external connectivity on endpoint hopeful_bardeen: Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: permission denied.
Significado: Este no es “ya en uso”; es “permiso denegado”, pero los operadores a menudo lo malinterpretan como un problema de puerto.
El modo rootless no puede enlazar puertos privilegiados (<1024) sin ayuda.
Decisión: Publica un puerto alto (por ejemplo, 8080) y colócale un proxy privilegiado delante, o configura una solución deliberada basada en capacidades.
Segundo chiste corto, y volvemos al trabajo: Reiniciar Docker para arreglar un conflicto de puertos es como reiniciar la oficina para encontrar tu grapadora. Funciona, pero harás enemigos.
Elige la corrección limpia: un árbol de decisiones
“Corregir limpiamente” no es “kill -9 y seguir”. Una corrección limpia es aquella donde entiendes propiedad, impacto y recurrencia. Usa este árbol de decisiones.
1) ¿El puerto lo posee un servicio del host que realmente necesitas?
- Sí, y debe quedarse en ese puerto: No publiques Docker en el mismo puerto. Publica en otro puerto del host y usa un proxy inverso (o balanceador) para enrutar.
- Sí, pero puede moverse: Cambia la configuración del servicio del host y despliega en una ventana controlada. Actualiza monitorización y reglas de firewall.
- No, es inesperado: Rastrear quién lo inició (PID → padre → archivo de unidad o contenedor) y elimínalo apropiadamente.
2) ¿El puerto lo posee otro contenedor?
- Es el mismo servicio (despliegue de reemplazo): Haz un swap controlado. Con Compose, eso puede significar detener el servicio antiguo antes de iniciar el nuevo, o usar puertos publicados diferentes para blue/green.
- Es un servicio distinto: Negocia puertos. No construyas un sistema no documentado de “quien arranca primero gana”.
3) ¿El propietario es systemd vía una unidad socket?
- Sí: Detén/desactiva la unidad
.socket, o cámbiala para que escuche en otra dirección/puerto. Detener la unidad.servicesola es teatro.
4) ¿Es un artefacto de red de Docker obsoleto?
- Probable: Valida la vista de Docker (
docker ps,docker network inspect) frente a la vista del kernel (ss). Si discrepan, reinicia Docker de forma controlada.
5) ¿Realmente necesitas enlazar a 0.0.0.0?
Publicar en todas las interfaces es cómodo y a menudo incorrecto. Si un servicio es solo para acceso local (métricas, UIs de administración),
enlaza a 127.0.0.1 o a una interfaz de gestión específica y mantenlo fuera de Internet público.
Tres mini-historias corporativas (dolor, aprendizaje, papeleo)
Mini-historia 1: El incidente causado por una suposición errónea
Un equipo heredó un host “sencillo”: una VM, Docker instalado, un puñado de contenedores y un proxy inverso. El runbook decía que la aplicación vivía en el puerto 8080.
Así que el on-call asumió que 8080 era el puerto de la app. Razonable. Equivocado.
Durante una actualización rutinaria, desplegaron un contenedor nuevo con -p 8080:80 y pulsaron “puerto ya asignado”. Hicieron lo que la gente hace cuando el reloj corre:
escogieron otro puerto (8081), actualizaron el proxy inverso y dieron por resuelto. El tráfico volvió.
Dos horas después saltó otra alerta: callbacks internos estaban fallando. Algún sistema upstream tenía una dependencia codificada en duro a http://host:8080.
Esa dependencia existía porque meses antes alguien saltó el proxy inverso “temporalmente”, y “temporal” se convirtió en infraestructura.
La suposición errónea no era sobre Docker. Era sobre propiedad y contratos de interfaz. El puerto 8080 no era “la app”. Era “el contrato contra el que otros equipos programaron”.
Cuando lo entendieron, la solución fue aburrida: volver a poner el proxy inverso en 8080, mover el servicio interno a un puerto no conflictivo y documentar el contrato.
La acción de seguimiento fue la verdadera victoria: añadieron una comprobación de inventario de listeners en los despliegues. Si cambia la propiedad de un puerto, el despliegue falla en voz alta antes que produzca un incidente.
Mini-historia 2: La optimización que salió mal
Otra organización decidió “optimizar” la red del host. Querían menos piezas móviles, así que cambiaron servicios sensibles a latencia a --network host.
Eso elimina NAT y puede reducir overhead. También elimina las vallas de seguridad.
El primer mes fue bien. Luego un desarrollador lanzó un sidecar con puerto por defecto 9090, también en red host.
En un nodo, 9090 ya lo usaba un exportador de métricas. En otro, no. Así que el despliegue tuvo éxito “a veces”.
El modo de fallo fue desagradable: el contenedor arrancaba en nodos donde el puerto estaba libre, y fallaba en otros con “bind: address already in use”.
O peor, el sidecar arrancaba primero y robaba el puerto, y el exportador fallaba. Mismo puerto, dos servicios, orden de arranque no determinista.
La “optimización” no era inherentemente mala, pero requería disciplina que no tenían: asignación explícita de puertos, validación a nivel de nodo y una política contra puertos por defecto.
Revirtieron host networking para la mayoría de servicios y lo mantuvieron solo donde estaba justificado—y documentaron los puertos permitidos por nodo.
Mini-historia 3: La práctica aburrida pero correcta que salvó el día
Una plataforma relacionada con pagos operaba múltiples hosts Docker con gestión de cambios estricta. Nada emocionante. También mantenían un hábito sencillo:
cada host tenía un job nocturno que capturaba un snapshot de sockets en escucha y los mapeaba a nombres de unidades y contenedores.
Una mañana, un host empezó a rechazar un despliegue con el conocido error de bind. El on-call sacó el snapshot de la noche anterior y lo comparó con “ahora”.
El puerto 443 tenía un listener nuevo propiedad de una unidad socket de systemd que no estaba en la línea base. Eso redujo la búsqueda de “cualquier cosa en el host” a “cambios recientes del sistema”.
El culpable fue una actualización de paquete que habilitó una unidad socket por defecto para una UI web empaquetada. El servicio ni siquiera estaba en ejecución todavía—systemd retenía el puerto preventivamente.
Sin el snapshot, habrían perseguido networking de Docker e iptables durante una hora.
La corrección fue trivial: desactiva la unidad socket inesperada, reintenta el despliegue y añade un pin de paquete hasta que puedan reubicar la UI.
La práctica era aburrida y efectiva: sabe qué escucha el host y cuándo cambia.
Errores comunes: síntoma → causa raíz → solución
Estos son patrones que aparecen repetidamente en producción. Los síntomas suelen parecer idénticos. Las soluciones no lo son.
1) Síntoma: Docker dice “port is already allocated” pero no encuentras un proceso
- Causa raíz: El listener está sólo en IPv6 (
[::]:PORT) o enlazado a una IP específica que no comprobaste, o lo retienedocker-proxy. - Solución: Usa
ss -ltnpsin filtros primero, luego revisa IPv4 e IPv6. Si esdocker-proxy, mapea al contenedor vía su línea de comando odocker network inspect.
2) Síntoma: Detuviste el servicio pero el puerto sigue ocupado
- Causa raíz: Activación por socket de systemd. La unidad
.socketposee el puerto, no el daemon que detuviste. - Solución: Detén/desactiva la unidad socket:
systemctl stop something.socketysystemctl disable something.socket.
3) Síntoma: Deploy de Compose funciona en un host, falla en otro
- Causa raíz: Consumidores ocultos de puertos difieren entre hosts (exportadores de métricas, servicios de desarrollo local, valores por defecto de la distro).
- Solución: Estandariza listeners base y verifícalos en CI/CD preflight. O deja de publicar puertos fijos por host y pon servicios detrás de un proxy inverso.
4) Síntoma: “Arreglaste” el bind cambiando a un puerto alto al azar, luego fallan sistemas downstream
- Causa raíz: El número de puerto formaba parte de un contrato (clientes con hardcode, reglas de firewall, allowlists, verificaciones de monitorización).
- Solución: Restaura el puerto del contrato y mueve el servicio en conflicto. Si debes cambiar el contrato, coordínalo y versiona ese cambio como una API.
5) Síntoma: Docker publica en IPv4, pero el servicio sólo es accesible en localhost (o viceversa)
- Causa raíz: Desajuste de binding: el host enlaza a
127.0.0.1mientras clientes esperan acceso externo, o el proceso dentro del contenedor sólo escucha en127.0.0.1en lugar de0.0.0.0. - Solución: Alinea las capas de binding. Para contenedores, asegúrate que el proceso escucha en
0.0.0.0dentro del contenedor. Para publicación en el host, usa la IP correcta en-p(p. ej.-p 127.0.0.1:8080:80).
6) Síntoma: Tras eliminar un contenedor, el puerto sigue asignado
- Causa raíz:
docker-proxyhuérfano o estado del demonio atascado. - Solución: Confirma que ningún contenedor publica el puerto. Si ninguno lo hace, reinicia Docker de forma controlada y vuelve a comprobar listeners. Si persiste, investiga procesos residuales y logs de Docker.
7) Síntoma: “bind: permission denied” al intentar publicar puerto 80 en Docker rootless
- Causa raíz: Restricción de puertos privilegiados en modo rootless.
- Solución: Publica un puerto alto y pon un proxy inverso privilegiado delante, o implementa una solución basada en capacidades de forma deliberada (y documenta las implicaciones de seguridad).
8) Síntoma: No puedes enlazar a 0.0.0.0:PORT, pero enlazar a 127.0.0.1:PORT funciona
- Causa raíz: Algo ya posee el bind wildcard (
0.0.0.0) en ese puerto, o tu servicio intenta reclamar wildcard mientras otro servicio está enlazado a una dirección específica con flags exclusivos. - Solución: Inspecciona listeners por direcciones. Decide si quieres el puerto en todas las interfaces; enlaza explícitamente a la IP necesaria, o mueve el servicio en conflicto.
Listas de verificación / plan paso a paso
Checklist A: Cuando solo necesitas que Docker arranque (sin convertirlo en problema de otro)
- Lee la línea de error. Anota protocolo, dirección y puerto (
0.0.0.0:PORTvs127.0.0.1:PORT). - Ejecuta
sudo ss -ltnp 'sport = :PORT'(o equivalente UDP) e identifica PID/proceso. - Si es un contenedor:
docker psy localiza el mapeo de puertos; detén/elimina el contenedor correcto. - Si es un servicio del host: encuentra la unidad con
systemctl status PID; decide mover vs detener. - Si es systemd socket: detén/desactiva la unidad
.socket. - Vuelve a ejecutar tu inicio de Docker con una dirección de enlace explícita si procede (solo loopback para herramientas internas).
- Verifica con
curllocalmente y desde un host par si el servicio debe ser externo.
Checklist B: Corrección limpia para producción (la que puedas explicar después)
- Inventario de listeners actual: captura la salida de
ss -ltnpantes de cambios. - Identifica la propiedad: mapea PID a unidad/contenedor y documenta en la ticket de cambio.
- Define el puerto contrato: qué clientes dependen de él? ¿Está en reglas de firewall, allowlists, monitorización o DNS?
- Elige una estrategia:
- Mover el puerto publicado del contenedor.
- Mover el puerto del servicio host.
- Introducir un proxy inverso y mantener estable el puerto externo.
- Enlazar servicios a interfaces distintas (público vs gestión).
- Despliega con plan de rollback: “Si X falla, restaura Y listener y revierte la configuración Z.” No es opcional.
- Validación post-cambio: el listener existe, la app responde, la monitorización pasa y no aparecieron listeners inesperados.
- Prevén recurrencia: añade una comprobación preflight de puertos en CI/CD o en el aprovisionamiento del host.
Checklist C: Plan específico para Compose (los puertos son política, no decoración)
- Localiza el mapeo en
docker-compose.ymlbajoports:. - Busca duplicados entre servicios (fallo común de copiar/pegar).
- Asegúrate de que tus puertos objetivo no están usados por otras stacks en el mismo host.
- Si necesitas múltiples instancias de la misma stack, no reutilices los mismos puertos del host. Paramétrizalos.
- Considera eliminar puertos publicados para servicios solo internos y conéctalos vía redes de Docker en su lugar.
FAQ
1) ¿Por qué Docker dice “port is already allocated” en lugar de mostrar el proceso?
Docker informa el error del kernel desde la llamada bind. El kernel no aporta “detalles amables de propiedad” en esa respuesta de syscall.
Usa ss o lsof para identificar el PID propietario.
2) Ejecuté netstat y no mostró nada, pero Docker sigue fallando. ¿Por qué?
Puede que estés comprobando el protocolo equivocado (UDP vs TCP), la familia de direcciones equivocada (IPv4 vs IPv6), o filtrando mal.
Además, algunas versiones de netstat no muestran la propiedad de procesos sin root. Prefiere ss -ltnp con sudo.
3) ¿Pueden dos contenedores publicar el mismo puerto del host si se enlazan a IPs distintas?
Sí, si publicas explícitamente a IPs de host distintas, por ejemplo:
uno enlaza 127.0.0.1:8080 y otro 192.168.1.10:8080.
Si cualquiera enlaza a 0.0.0.0:8080, bloquea todas las direcciones IPv4 para ese puerto.
4) ¿Es seguro reiniciar Docker para arreglar esto?
A veces limpia proxies huérfanos o reconcilia estado. También interrumpe contenedores en ejecución y puede cortar conexiones.
En producción, trátalo como un reinicio de servicio dentro de una ventana de cambio. Si puedes arreglar el proceso propietario real, haz eso.
5) ¿Qué hago si el puerto lo tiene systemd (PID 1)?
Normalmente es una unidad .socket. Detén y desactiva la unidad socket. Detener el servicio relacionado no basta porque systemd mantiene el socket abierto.
6) ¿TIME_WAIT podría causar “address already in use”?
No para un bind de escucha típico. TIME_WAIT trata sobre conexiones recientemente cerradas.
El habitual “address already in use” en publicación de puertos Docker es un listener real, no churn de conexiones.
7) Uso Docker rootless y no puedo publicar el puerto 80. ¿Es lo mismo?
No. Normalmente es “permission denied” debido a reglas de puertos privilegiados. La solución limpia es publicar un puerto alto y poner un proxy inverso privilegiado delante,
o configurar deliberadamente una solución basada en capacidades (y asumir la seguridad resultante).
8) ¿Por qué Compose a veces falla durante actualizaciones rolling con conflictos de puertos?
Compose no hace actualizaciones rolling como un orquestador. Si intentas ejecutar “viejo” y “nuevo” contenedores simultáneamente con los mismos puertos publicados,
el segundo no podrá enlazar. Usa puertos diferentes para blue/green, o detén el contenedor antiguo antes de iniciar el nuevo.
9) ¿Cómo evito esto para siempre?
No puedes. Pero puedes reducir sorpresas:
mantén un documento de asignación de puertos por host (o por entorno), usa proxies inversos para puertos externos estables, y añade una comprobación preflight que falle los despliegues
cuando los puertos requeridos ya estén ocupados.
10) ¿El NodePort de Kubernetes causa el mismo problema de “address already in use”?
Puede, pero la propiedad cambia: kube-proxy y listeners a nivel de nodo pueden reservar puertos. El método de diagnóstico es el mismo: comprueba listeners con ss,
y mapea el propietario al componente del orquestador.
Conclusión: siguientes pasos que no te despertarán a las 3 AM
“bind: address already in use” es el kernel haciendo su trabajo. Tu trabajo es dejar de tratarlo como una rabieta aleatoria de Docker.
Identifica el listener con ss, mapea a una unidad o contenedor, y elige una solución que respete contratos y el radio de impacto.
Pasos prácticos siguientes:
- Añade un preflight de despliegue que compruebe puertos requeridos del host con
ssy falle rápido. - Deja de publicar todo en
0.0.0.0por defecto. Enlaza herramientas internas al loopback. - Documenta qué servicio “posee” cada puerto externo relevante y trata los cambios como cambios de API.
- Si hay unidades socket de systemd en juego, audítalas—los puertos pueden estar reservados incluso cuando “el servicio está detenido”.
- Cuando el estado de Docker y el estado del kernel discrepen, reinicia Docker solo como operación controlada, no por superstición.