Despliegas un servicio, intenta enlazar y el kernel responde con la verdad más poco útil en informática: bind: Address already in use. Suena la alarma. Alguien dice “simplemente reinícialo”. Otro dice “probablemente es DNS”. Ninguno es tu amigo en este momento.
En Debian 13 tienes todas las herramientas necesarias para identificar exactamente quién ocupa un puerto—proceso, unidad systemd, contenedor, activación por socket, incluso un namespace obsoleto—y luego resolver el conflicto sin daños colaterales. El truco es saber el orden en que mirar y qué te está diciendo cada herramienta.
Playbook de diagnóstico rápido
Esta es la secuencia para “detener la hemorragia”. Está optimizada para encontrar al propietario rápido, no para enseñar. La enseñanza viene después.
Primero: confirma qué está fallando realmente (registros del servicio)
No empieces con escáneres de puertos. Empieza por el gestor de servicios. Si systemd está implicado, a menudo te dirá la dirección y el puerto exactos que fallaron al enlazar.
Segundo: identifica el listener (ss)
Usa ss primero. Es moderno, rápido y muestra la vista del kernel de los sockets con la asociación de procesos.
Tercero: mapea el PID al propietario real (unidad systemd / contenedor)
La propiedad del PID no es lo mismo que “quién causó esto en producción”. Quieres la unidad, el paquete o la imagen de contenedor detrás de él.
Cuarto: comprueba activación por socket de systemd y reenviadores de puertos
El puerto puede estar ocupado por una unidad socket, no por el demonio que piensas. O por un proxy (Envoy, HAProxy, nginx) que alguien olvidó mencionar.
Quinto: elige una solución limpia
Prefiere deshabilitar el listener incorrecto, corregir la configuración o migrar el servicio a un puerto dedicado. Evita “kill -9” a menos que disfrutes de los postmortems.
Hechos y contexto interesantes (por qué esto sigue ocurriendo)
- Hecho 1: La cadena de error proviene de
EADDRINUSE, un errno POSIX usado porbind(2)cuando una tupla dirección/puerto local ya está reservada. - Hecho 2: Antes de que
ssfuera común, los administradores usabannetstat(de net-tools). Debian ha estado empujando a la gente fuera de net-tools durante años, y Debian 13 está firmemente en la era “usa iproute2”. - Hecho 3: La activación por socket de systemd puede enlazar puertos antes de que el servicio arranque. Eso significa que el propietario del puerto puede ser systemd, no tu demonio.
- Hecho 4: IPv6 puede “cubrir” IPv4 mediante sockets mapeados v6 dependiendo de
net.ipv6.bindv6only. Crees que enlazas IPv4, pero un listener IPv6 está ocupando el puerto. - Hecho 5: Los puertos privilegiados (por debajo de 1024) históricamente requerían root en Unix. Linux moderno puede conceder esa capacidad a un binario mediante file capabilities, lo que cambia quién puede enlazar qué—y quién puede entrar en conflicto.
- Hecho 6: Un conflicto de puerto puede ser causado por un namespace de red completamente distinto (contenedor, systemd-nspawn). Las herramientas en el host pueden no verlo a menos que mires en el namespace correcto.
- Hecho 7:
SO_REUSEADDRestá ampliamente malentendido. No significa “dos programas diferentes pueden escuchar el mismo puerto TCP”. Eso esSO_REUSEPORT, y tiene efectos secundarios complejos. - Hecho 8: UDP se comporta de manera distinta. Múltiples sockets UDP a veces pueden enlazar el mismo puerto dependiendo de opciones y direcciones, lo que puede hacer que “quién lo posee” sea menos obvio.
- Hecho 9: “Nada está escuchando pero bind falla” a veces significa que tu app intenta enlazar a una IP que aún está en la máquina pero no en la interfaz que piensas (o está gestionada por VRRP, keepalived o un agente de cloud).
Qué significa realmente “Address already in use” en Linux
Cuando un servidor arranca, normalmente hace alguna versión de: crear un socket, ajustar opciones, luego bind() a una dirección/puerto local, luego listen(). Si el kernel no puede reservar esa combinación dirección/puerto, devuelve EADDRINUSE. Tu programa imprime el error y sale (o reintenta si es educado).
Aquí está la parte importante: “Dirección” incluye más que un número de puerto. Puede significar:
- TCP vs UDP: El puerto TCP 53 y UDP 53 son sockets diferentes. Uno puede estar en uso mientras el otro está libre.
- Específico de IP vs comodín: Enlazar a
127.0.0.1:8080es distinto a enlazar a0.0.0.0:8080(todo IPv4). Pero un enlace comodín puede bloquear un enlace específico, dependiendo de cómo ya esté enlazado. - Comodín IPv6:
[::]:8080puede, en algunos sistemas, aceptar también conexiones IPv4 a menos que el kernel esté configurado como v6-only. - Namespaces de red: Si estás dentro de un namespace de contenedor, “puerto 8080” no es necesariamente el 8080 del host—a menos que lo hayas publicado.
Si solo sacas una lección de esta sección: captura siempre el destino completo del bind desde los registros—protocolo, IP y puerto—antes de perseguir fantasmas.
Una idea parafraseada de Richard Cook (investigador en fiabilidad): Los fallos ocurren en los huecos entre cómo se imagina el trabajo y cómo se hace realmente.
La propiedad de puertos es uno de esos huecos.
Tareas prácticas: comandos, salidas, decisiones (12+)
Estas son las tareas que ejecutas en un host Debian 13 cuando un servicio no puede arrancar por “Address already in use”. Cada una incluye qué significa la salida y qué decisión tomar a continuación.
Tarea 1: Lee los registros de la unidad que falla (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'.
Significado: Obtienes el objetivo de bind: 0.0.0.0:8080. Ese es el puerto comodín IPv4 completo. Ahora puedes buscar con precisión.
Decisión: Ve a encontrar quién ya está escuchando en TCP/8080, en comodín IPv4 o IPv6.
Tarea 2: Encuentra listeners con ss (rápido, preciso)
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))
Significado: El puerto TCP 8080 ya está enlazado en todas las direcciones IPv4 por nginx PID 1312.
Decisión: Determina si nginx debe poseer 8080. Si no, ajusta la configuración de nginx o detén/deshabilita la unidad.
Tarea 3: Confirma que IPv6 no esté también involucrado
cr0x@server:~$ sudo ss -H -ltnp 'sport = :8080' -6
LISTEN 0 4096 [::]:8080 [::]:* users:(("nginx",pid=1312,fd=9))
Significado: nginx también está escuchando en el comodín IPv6. Incluso si arreglas IPv4, IPv6 puede seguir colisionando según el comportamiento de bind de tu app.
Decisión: Decide si tu nuevo servicio debe escuchar también en IPv6 y asegúrate de que nginx se retire por completo del puerto, no “medio arreglado”.
Tarea 4: Traduce PID a unidad systemd (el propietario real)
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
Significado: Es un servicio gestionado, no un proceso aleatorio. Excelente: puedes arreglar esto de forma limpia.
Decisión: Revisa la configuración de nginx para un listener en 8080, luego cámbiala o elimínala y recarga.
Tarea 5: Localiza la directiva de binding en nginx
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'listen\s+8080'
47: listen 8080 default_server;
98: listen [::]:8080 default_server;
Significado: nginx está configurado explícitamente para usar 8080 en ambas pilas.
Decisión: O cambias nginx a otro puerto, o reasignas tu app a un puerto distinto, o pones la app detrás de nginx y mantienes 8080 para nginx. Elige uno, no “compartan”.
Tarea 6: Si no es un servicio gestionado, identifica el paquete o padre
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
Significado: Postgres está escuchando solo en localhost, ocupando 5432.
Decisión: Si intentaste iniciar otro Postgres o una app que quiere 5432, para uno u otro. Iniciar “otro Postgres en 5432” no es un plan.
Tarea 7: Usa lsof cuando ss no muestre lo que esperas
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)
Significado: Misma historia, herramienta diferente. lsof es más lento pero a veces más claro, especialmente bajo presión.
Decisión: Si las herramientas discrepan, confía en la vista del kernel pero verifica el namespace (ver más adelante). La mayor parte del tiempo, la discrepancia significa que miras en namespaces diferentes o filtraste mal.
Tarea 8: Comprueba si la activación por socket de systemd mantiene el puerto
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 [::]:*
Significado: El puerto podría ser propiedad de una unidad socket, que puede seguir escuchando incluso si el servicio está detenido. Este es un clásico “lo detuve, ¿por qué sigue en uso?”.
Decisión: Gestiona la unidad .socket, no solo el .service. Deshabilita/máscara el socket si no quieres que systemd sea el propietario del puerto.
Tarea 9: Detén y deshabilita la unidad correcta (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".
Significado: Detener el servicio no hizo nada porque systemd mantuvo el socket. Detener el socket liberó el puerto.
Decisión: Si nginx no debe gestionar ese puerto, deshabilita el socket (y asegúrate de que el servicio tampoco esté habilitado involuntariamente).
Chiste 1: Si “arreglas” un conflicto de puertos reiniciando, no lo solucionaste—solo reciclaste el misterio.
Tarea 10: Confirma qué dirección IP está enlazando tu app
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
Significado: Solo puedes enlazar a direcciones que posees (o que poseerás mediante un gestor como keepalived). Si tu app intenta enlazar a 10.10.5.99 y no la tienes, obtendrás otro error (usualmente Cannot assign requested address), pero la gente frecuentemente interpreta mal los registros y persigue el problema equivocado.
Decisión: Valida que la dirección de bind coincida con la realidad. Si es una VIP, confirma que la VIP está presente en este nodo.
Tarea 11: Captura un listener “oculto” en otro namespace de red (contenedores)
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))
Significado: El puerto del host lo mantiene docker-proxy (o a veces reglas nftables sin docker-proxy, según la configuración). Tu aplicación real está dentro de un contenedor, pero el bind en el lado del host es lo que bloquea tu servicio.
Decisión: Encuentra qué contenedor publicó el puerto. No mates docker-proxy; arregla el mapeo del contenedor.
Tarea 12: Identifica el contenedor que publicó el puerto (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"}]}
Significado: El contenedor prometheus posee el host 9090.
Decisión: Si tu nuevo servicio necesita 9090, mueve Prometheus (recomendado: mantener 9090 para Prometheus y mover tu servicio), o cambia el puerto publicado y actualiza las configuraciones de scrape.
Tarea 13: Identifica listeners creados por una sesión de usuario (no root, no 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
Significado: Un humano está ejecutando un servidor de desarrollo en producción. Esto ocurre más de lo que cualquiera admite.
Decisión: Coordina. O mueves el proceso de desarrollo, o reservas el puerto para producción y dices a la gente que use un puerto no conflictivo ligado a localhost (o, mejor: un host de desarrollo).
Tarea 14: Maneja conflictos UDP (DNS, métricas, servidores de juego)
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))
Significado: UDP/53 ya está enlazado en localhost por un resolvedor. Si intentas iniciar un servidor DNS, entrarás en conflicto.
Decisión: Decide si este host debe ejecutar un servidor DNS. Si sí, puede que necesites reconfigurar el resolvedor local para que deje de enlazar, o enlazar tu servicio DNS a una interfaz/IP específica que no entre en conflicto (con cuidado).
Tarea 15: Verifica los Listen* y puertos configurados de un servicio vía 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
Significado: La unidad socket es la fuente del bind. Esta es la evidencia clave cuando un puerto “sigue reapareciendo”.
Decisión: Cambia/sobrescribe esta unidad, o deshabilítala/máscala.
Tarea 16: Confirma que el puerto está realmente libre tras un cambio
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))
Significado: La propiedad es limpia: el proceso esperado está ahora escuchando.
Decisión: Pasa a comprobaciones de salud, enrutamiento y lo que sea que los dioses de las incidencias hayan puesto en cola.
Sutilezas de systemd: sockets, unidades, escuchas persistentes
En Debian 13, systemd es el centro de gravedad. Incluso si piensas que “solo ejecutas un binario”, probablemente lo haces mediante una unidad, un timer, un socket o un servicio de contenedor.
La activación por socket es buena—hasta que te olvidas de que existe
La activación por socket significa que systemd enlaza el puerto y luego lanza el servicio bajo demanda y le pasa el descriptor de archivo. Mejora la latencia de arranque para servicios orientados a peticiones y puede suavizar reinicios. También crea un modo de fallo: detienes un servicio, pero el puerto queda ligado por PID 1.
Si ves users:(("systemd",pid=1,...)) en la salida de ss para tu puerto, no discutas. Encuentra la unidad socket:
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 [::]:*
Enmascarar: la opción nuclear que a veces es correcta
Deshabilitar una unidad impide que arranque al iniciar, pero otra cosa aún puede iniciarla manualmente o como dependencia. Enmascarar la bloquea completamente vinculándola a /dev/null. En respuesta a incidentes, enmascarar puede ser la acción correcta cuando un listener indeseado sigue resucitando.
cr0x@server:~$ sudo systemctl mask --now nginx.socket
Created symlink /etc/systemd/system/nginx.socket → /dev/null.
Regla de decisión: Deshabilita cuando cambias comportamiento previsto y tienes tiempo para hacerlo correctamente. Máscara cuando necesitas garantizar que no volverá (y limpiarás después).
Los drop-ins son mejor que editar archivos de unidad del proveedor
En Debian, las unidades del proveedor viven bajo /lib/systemd/system. Editarlas funciona hasta la próxima actualización del paquete, cuando tu arreglo se elimina amablemente.
Crea un override en su lugar:
cr0x@server:~$ sudo systemctl edit nginx.socket
[Socket]
ListenStream=
ListenStream=127.0.0.1:8080
Significado: La línea vacía ListenStream= reinicia la lista, luego estableces la nueva. Esta es la forma de systemd de asegurarse de que realmente quisiste “reemplazar”, no “añadir”.
Decisión: Usa drop-ins para comportamiento estable a través de actualizaciones. Editar unidades del proveedor es un parche temporal que debes tratar como tal.
Contenedores y orquestación: Docker, Podman, Kubernetes
Los conflictos de puertos en entornos contenerizados rara vez son “dos demonios quieren 8080”. Suelen ser “algo publicó 8080 en el host hace meses y todos lo olvidaron”. Los contenedores facilitan entregar software; también facilitan ocupar puertos indefinidamente.
Docker y publicación de puertos en el host
Cuando ejecutas -p 8080:8080, estás reclamando explícitamente el puerto del host. En muchas configuraciones verás docker-proxy como propietario del socket. En otras, las reglas nftables manejan el reenvío, pero el puerto del host aún puede aparecer reservado según el modo.
El flujo de trabajo limpio es:
- Identifica que un proxy es el listener (
ssmuestra docker-proxy). - Mapéalo a un contenedor (
docker ps,docker inspect). - Cambia el puerto publicado o elimina el contenedor.
Podman: misma idea, distinta tubería
Podman puede ejecutar contenedores sin root. El binding de puertos sin root introduce un giro: los procesos pueden vivir bajo una sesión de usuario, y el reenvío de puertos puede hacerlo slirp4netns o pasta. El síntoma sigue siendo el mismo: tu puerto del host está ocupado.
Si sospechas de 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
Decisión: Si es sin root y está ligado a un usuario, puede que necesites coordinar con esa sesión de usuario o deshabilitar lingering para esa cuenta.
Kubernetes: NodePort y hostNetwork
Kubernetes añade un tipo especial de caos:
- NodePort reclama puertos en un rango, y puedes colisionar si algo más escucha ahí.
- hostNetwork: true hace que los pods enlacen directamente en la pila de red del nodo.
- DaemonSets significan “está en todos los nodos”, lo cual es genial cuando es intencional y extremadamente molesto cuando no lo es.
En un nodo con kubelet, ss mostrará el proceso propietario (a veces el proceso de la aplicación, a veces un proxy). Tu trabajo es mapearlo al pod y la carga de trabajo. Los comandos exactos dependen de tu acceso al clúster, pero el diagnóstico en el lado del host no cambia: identifica el listener primero.
IPv4/IPv6 y “está escuchando, pero no donde buscaste”
La confusión clásica: ejecutas ss -ltnp, no ves tu puerto, pero la app aún falla con “Address already in use”. Luego lo ejecutas de nuevo y ves algo en [::]. Dices “no usamos IPv6”. Al kernel no le importan tus creencias.
Entiende los listeners comodín
Estos son los patrones que debes reconocer:
0.0.0.0:PORTsignifica todas las direcciones IPv4.[::]:PORTsignifica todas las direcciones IPv6. Dependiendo del sysctl, también puede aceptar conexiones IPv4 mapeadas.127.0.0.1:PORTsignifica solo localhost (usualmente lo más seguro para endpoints administrativos internos).
Comprueba bindv6only cuando el comportamiento sorprende
cr0x@server:~$ sysctl net.ipv6.bindv6only
net.ipv6.bindv6only = 0
Significado: Con 0, un socket comodín IPv6 también puede aceptar conexiones IPv4 vía direcciones mapeadas v4 en muchos sistemas. Eso puede hacer que [::]:8080 bloquee las expectativas de 0.0.0.0:8080 dependiendo de cómo las aplicaciones ajusten opciones.
Decisión: No “arregles” esto cambiando sysctls durante un incidente a menos que entiendas el radio de impacto. Arréglalo a nivel de configuración de la aplicación/servicio: enlaza explícitamente a IPv4 o IPv6 según lo pretendido.
Carreras, reinicios y mitos sobre TIME_WAIT
La gente culpa a TIME_WAIT por fallos de bind igual que culpa a “la red” por consultas lentas. A veces está involucrado, pero rara vez es la causa de que un servidor no pueda enlazar su puerto de escucha.
TIME_WAIT suele afectar a conexiones salientes
Los sockets en TIME_WAIT son típicamente puertos efímeros del lado cliente, no puertos de escucha del servidor. Tu servidor normalmente puede volver a enlazar su puerto de escucha sin problema. Si no puede, casi seguro tienes un listener real, una unidad socket o un proceso atascado.
Cuando los reinicios provocan conflictos de bind
El verdadero problema de reinicio es dos instancias se solapan:
- systemd inicia una nueva instancia antes de que la antigua salga realmente (Type=forking mal configurado, seguimiento de PID incorrecto).
- Una app se demoniza pero la unidad está escrita para un proceso que no se demoniza, así que systemd piensa que murió y la reinicia—creando múltiples intentos de bind.
- Un script wrapper inicia el servidor real y sale inmediatamente.
Para detectar esto, observa múltiples procesos con el mismo binario o bucles de reinicio rápidos en el 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.
Decisión: Si hay un bucle de reinicio, detén la unidad para estabilizar el host y luego arregla el conflicto. De lo contrario estarás persiguiendo un objetivo que se mueve mientras systemd genera fallos repetidamente.
Soluciones limpias que puedes defender en una revisión
Puedes “liberar el puerto” de muchas maneras. La mayoría son perezosas. El objetivo es arreglar el modelo de propiedad para que el conflicto no vuelva en el próximo reinicio, redeploy o ingeniero bienintencionado del viernes por la tarde.
Opción A: Mover el servicio a un puerto correcto (y documentarlo)
Si dos servicios quieren 8080 porque nadie eligió un plan de puertos, elige uno ahora. Ponlo en la gestión de configuración. Inclúyelo en las etiquetas de monitorización. Hazlo aburrido.
Opción B: Poner un servicio detrás de un reverse proxy
Si nginx ya está en 80/443 y tu app quiere 8080, la arquitectura limpia suele ser: nginx posee los puertos públicos; las apps viven en puertos altos privados o sockets Unix; nginx enruta.
Sé consistente. “A veces enlazamos apps directamente a 0.0.0.0” es cómo terminas con guerras de puertos sorpresivas.
Opción C: Parar/deshabilitar el servicio incorrecto (service + socket si hace falta)
Si el puerto lo posee algo que no quieres, elimínalo del grafo de arranque:
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".
Decisión: Deshabilitar ambos evita el baile “lo detuve pero volvió”.
Opción D: Hacer binds explícitos (evita comodines cuando no los necesitas)
Enlazar a 0.0.0.0 es conveniente y frecuentemente erróneo. Para endpoints administrativos, enlaza a localhost. Para servicios internos, enlaza a la IP de la interfaz interna o a un VRF dedicado. Para servicios públicos, enlaza al VIP del balanceador de carga o deja que el proxy lo posea.
Opción E: Usa file capabilities para puertos privilegiados en lugar de ejecutar como root
Si tu servicio necesita 80/443 pero no debería ejecutarse como root, establece capacidades:
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
Significado: El binario puede enlazar puertos privilegiados sin privilegios root completos.
Decisión: Usa esto cuando lo necesites, pero regístralo—las capacidades son fáciles de olvidar y cambian tu postura de seguridad. Además: las capacidades no previenen conflictos; solo cambian quién puede crearlos.
Opción F: Reservar puertos y hacer cumplir la política
En entornos serios, mantienes un registro de puertos por cada rol de host o clúster. Lo aplicas en CI, en charts helm, en plantillas systemd. Sin eso, los conflictos de puertos no son “incidentes”, son “inevitables”.
Chiste 2: La asignación de puertos sin registro es como sentarse en el almuerzo de una conferencia—todo el mundo pelea por la única mesa cerca del enchufe.
Errores comunes: síntoma → causa raíz → solución
1) “Detuve el servicio pero el puerto sigue en uso”
Síntoma: systemctl stop foo.service tiene éxito, pero ss aún muestra el puerto en LISTEN.
Causa raíz: Una unidad foo.socket está enlazando el puerto (activación por socket), o otra dependencia aún lo mantiene.
Solución: Revisa systemctl list-sockets. Detén/deshabilita/máscara la unidad socket. Verifica con ss que PID 1 ya no sea el listener.
2) “ss no muestra nada, pero mi app aún dice Address already in use”
Síntoma: Buscas con ss y no ves un listener en ese puerto.
Causa raíz: Buscaste el protocolo equivocado (UDP vs TCP), la familia de direcciones equivocada (IPv4 vs IPv6), o tus filtros lo excluyeron. Menos común: estás en un namespace de red distinto al del listener.
Solución: Busca en ambas pilas y protocolos: ss -ltnp, ss -lunp, incluye -6. Si hay contenedores involucrados, comprueba docker-proxy e inspecciona los mappings de contenedores.
3) “El puerto es propiedad de ‘systemd’ y no sé por qué”
Síntoma: ss muestra users:(("systemd",pid=1,...)).
Causa raíz: Una unidad socket está configurada para enlazarlo (a veces con un nombre que no esperabas, o traída por un paquete).
Solución: Identifica el socket vía systemctl list-sockets --all, luego systemctl cat NAME.socket. Deshabilita o sobrescribe su ListenStream/ListenDatagram.
4) “Funciona en IPv4 pero no en IPv6” (o viceversa)
Síntoma: El servicio arranca cuando se enlaza a 127.0.0.1 pero falla en 0.0.0.0, o falla solo al configurarlo para [::].
Causa raíz: Otro servicio está enlazado en una familia; o el comportamiento dual-stack de v6 está bloqueando v4.
Solución: Inspecciona listeners para ambas familias. Haz binds explícitos en las configuraciones. No confíes en valores por defecto.
5) “Cambiamos el puerto, pero sigue revirtiéndose”
Síntoma: Después de editar una unidad o config, el puerto antiguo vuelve tras actualizaciones/reboots.
Causa raíz: Editaste la configuración del proveedor bajo /lib, o una herramienta de gestión de configuración sobrescribe tu cambio.
Solución: Usa drop-ins de systemd bajo /etc/systemd/system. Si existe gestión de configuración, hazla la fuente de la verdad.
6) “Matar el PID lo arregló, pero ahora otras cosas están rotas”
Síntoma: Usaste kill -9 en el listener; el puerto se liberó; después descubres efectos secundarios.
Causa raíz: Mataste un componente compartido (proxy, ingress, agente de métricas) o un servicio supervisado que se reinició en el mismo puerto.
Solución: Mapea PID → unidad/contenedor antes de matar. Deténlo vía systemd/Docker para que permanezca parado (o cambia su config). Usa kill solo cuando hayas decidido que el radio de impacto es aceptable.
Listas de verificación / plan paso a paso
Lista: Identifica al propietario en menos de 2 minutos
- Lee el objetivo exacto del bind desde los registros (
systemctl statuso registros de la app). Captura protocolo, IP y puerto. - Ejecuta
ss -ltnp 'sport = :PORT'para TCP;ss -lunp 'sport = :PORT'para UDP. - Si no aparece nada, comprueba explícitamente IPv6 con
-6y asegúrate de no haber escrito mal el puerto. - Una vez tengas PID/comando, mapea a la unidad systemd con
systemctl statusyps. - Si el listener es systemd PID 1, lista los sockets y encuentra la unidad
.socket. - Si el listener es docker-proxy/contenerización, identifica el contenedor que publica el puerto.
Lista: Haz la corrección durable
- Decide qué servicio debe poseer el puerto (decisión de arquitectura, no una moneda al aire).
- Implementa cambios de configuración usando drop-ins o configuración gestionada, no editando archivos del proveedor.
- Detén/deshabilita al antiguo propietario (servicio y socket si es relevante).
- Inicia al propietario previsto y verifica con
ss. - Realiza una prueba de conectividad local (curl, nc) y verifica el enrutamiento externo si aplica.
- Añade una comprobación de monitorización que detecte listeners inesperados en puertos críticos.
Lista: Procedimiento seguro de “liberar puerto de emergencia”
- Detén el servicio que falla para evitar bucles de reinicio.
- Identifica al propietario actual con
ss/lsof. - Deténlo limpiamente usando su supervisor (systemd/Docker) en lugar de matar.
- Sólo si la parada limpia falla: envía SIGTERM, espera y luego considera SIGKILL.
- Documenta lo que hiciste y por qué. Tu yo futuro no recordará a las 3 a.m.
Tres micro-historias del mundo corporativo
Micro-historia 1: El incidente causado por una suposición errónea
El equipo tenía un despliegue simple: desplegar una nueva API interna en el puerto 8080, detrás de un reverse proxy existente. Todos “sabían” que 8080 era el puerto interno de las apps. Estaba en la cabeza de alguien y en una página wiki de hace dos años que nadie confiaba lo suficiente como para actualizar.
El despliegue falló con “Address already in use.” El ingeniero on-call hizo el chequeo estándar con ss y vio nginx escuchando en 8080. Eso parecía imposible—nginx “poseía” 80 y 443. Así que asumieron que ss mostraba un artefacto obsoleto y reiniciaron la máquina. (Volvió exactamente igual, porque claro que sí.)
Cuando la gente dejó de reiniciar cosas como forma de indagación, la causa fue vergonzosamente mundana: una migración anterior había movido una aplicación legada de 80 a 8080 temporalmente, y nginx quedó escuchando en 8080 como redirección. Fue un “temporal” que duró tres trimestres.
La solución no fue “matar nginx”. Fue una decisión arquitectónica pequeña: nginx obtiene puertos públicos; las apps internas obtienen un puerto alto asignado por servicio; 8080 no es “por defecto”, es “asignado”. Moveron el redirector legado bajo 80/443 y liberaron 8080 (o más exactamente, dejaron de tratar 8080 como una religión).
La acción post-incidente que realmente importó: un registro de puertos ligado a la propiedad del servicio. No una hoja de cálculo—algo aplicado en la revisión de configuración. El error dejó de ser misterioso porque la propiedad dejó de ser conocimiento tribal.
Micro-historia 2: La optimización que salió rana
Un ingeniero orientado al rendimiento quería reinicios más rápidos durante despliegues. Habilitó systemd socket activation para un pequeño servicio HTTP para que systemd mantuviera el socket y se lo entregara a un nuevo proceso con menos downtime. Idea inteligente, usada en los lugares adecuados.
Luego se olvidaron de que lo habían hecho. Meses después, otro equipo intentó desplegar un servicio distinto en el mismo puerto durante una consolidación. Detuvieron el servicio antiguo, vieron que estaba inactivo e intentaron arrancar el nuevo. Falló con “Address already in use.” ss mostraba PID 1 sujetando el puerto. La confusión escaló rápido.
En la realidad corporativa, el incidente no fue solo técnico. Se presentó una solicitud de cambio para “matar el proceso systemd que sujeta 8443.” Ahí sabes que estás en problemas: cuando alguien propone matar init por un conflicto de puertos.
La solución real fue limpia: detener y deshabilitar la unidad .socket, luego arrancar el nuevo servicio. La lección fue más aguda: la activación por socket es una característica de despliegue, no un interruptor olvidable. Si la activas, asumes la complejidad operativa que añade, incluyendo cómo cambia “lo que significa” que un servicio esté detenido.
Después codificaron una regla: los servicios activados por socket deben documentarse en la descripción de la unidad y monitorizarse con una alerta “socket ligado mientras el servicio está detenido”. Esa es la guardia aburrida que evita que la “optimización” se convierta en “misterio”.
Micro-historia 3: La práctica aburrida pero correcta que salvó el día
Un servicio cercano al almacenamiento corría en una flota Debian que también alojaba métricas, shippers de logs y un par de demonios legados que nadie osaba remover. Los puertos eran un campo minado. Pero este equipo tenía una práctica aburrida: antes de cualquier rollout ejecutaban un script preflight que comprobaba una lista pequeña de puertos reservados y verificaba el propietario esperado.
Durante una ventana de parches rutinaria, se desplegó un nuevo contenedor de métricas con un -p 9100:9100 por defecto porque el autor del chart asumió que Node Exporter siempre estaba “dentro del clúster”. En estos hosts, Node Exporter ya corría como servicio systemd. El contenedor agarró el puerto primero en un subconjunto de nodos por orden de scheduling. El rollout se vio “casi bien”, que es una gran manera de lastimarse.
El preflight lo detectó en el primer nodo del lote: el script vio que 9100 ya no era propiedad de la unidad esperada. El despliegue se pausó, no después de romper media flota—después de amenazar a un host.
La solución llevó minutos: quitar la publicación del puerto en el contenedor, usar el Node Exporter existente y evitar que el chart reclamara el puerto de nuevo. Sin drama, sin reinicios, sin sala de crisis. Solo una comprobación aburrida que previno una gran y aburrida caída.
Esto es lo que parece “excelencia operativa” en la práctica: no son heroicidades, es negarse a dejar sorpresas en producción.
Preguntas frecuentes
1) ¿Por qué ocurre “Address already in use” cuando no hay nada en ejecución?
Usualmente porque algo sí está en ejecución, solo que no es lo que esperas: una unidad socket de systemd, un proxy de contenedor, o un listener enlazado en IPv6 mientras buscabas en IPv4. Verifica con ss en todas las familias y revisa systemctl list-sockets.
2) ¿Cuál es el mejor comando para encontrar quién usa un puerto en Debian 13?
ss -ltnp (TCP) y ss -lunp (UDP). Usa un filtro preciso como 'sport = :8080' para no ahogarte en salida.
3) ¿Debería instalar net-tools para netstat?
No, salvo que sea por memoria muscular durante un incidente y aceptes el tradeoff. Debian 13 está pensado para herramientas iproute2. Aprende ss; vale la pena.
4) ¿Pueden dos procesos escuchar el mismo puerto TCP?
No de la manera simple que la gente espera. Hay casos avanzados con SO_REUSEPORT donde múltiples workers comparten un puerto, pero típicamente eso es dentro del diseño de un solo servicio. Para dos demonios no relacionados, trátalo como “no”.
5) ¿TIME_WAIT impide que mi servicio enlace su puerto?
Casi nunca para el puerto de escucha del servidor. TIME_WAIT suele referirse a conexiones salientes cerradas. Si bind falla, encuentra el listener real o la unidad socket.
6) ¿Por qué ss muestra que systemd posee el puerto?
Activación por socket. systemd está enlazando intencionalmente el puerto mediante una unidad .socket. Detén/deshabilita/máscara la unidad socket si quieres que el puerto se libere permanentemente.
7) ¿Cómo arreglo un conflicto de puertos sin reiniciar?
Identifica al propietario, deténlo vía su supervisor (systemd/Docker), deshabilita la ruta de autoarranque (service/socket/container), luego inicia el servicio previsto y verifica con ss.
8) ¿Cómo sé si el conflicto es IPv4 o IPv6?
Comprueba ambos: ss -ltnp 'sport = :PORT' y ss -ltnp -6 'sport = :PORT'. Busca 0.0.0.0 vs [::], y direcciones específicas como 127.0.0.1.
9) ¿Qué hago si el puerto es de un proceso de usuario y lo necesito de vuelta?
No empieces matando. Identifica al usuario y la sesión, coordina y establece política: los puertos de producción están reservados. Si debes recuperarlo rápido, detén el proceso con SIGTERM y luego sigue con una solución durable.
Conclusión: siguientes pasos que no te despertarán de madrugada
“Address already in use” no es un rompecabezas. Es una disputa de propiedad. El kernel te está diciendo que ya hay un inquilino en esa tupla dirección/puerto, y tu trabajo es averiguar si ese inquilino es legítimo.
Haz lo siguiente:
- Estandariza el uso de
sspara comprobaciones de propiedad de puertos e intégralo en tu runbook. - Cuando encuentres un PID, mapealo siempre a una unidad o contenedor. Arregla el sistema que lo inicia, no solo el proceso.
- Audita los servicios activados por socket y documéntalos. Si PID 1 posee un puerto, probablemente es intencional.
- Crea un registro de asignación de puertos para tu entorno (aunque sea pequeño). Aplícalo en las revisiones.
- Añade una comprobación ligera que valide que los puertos críticos son propiedad de los servicios esperados antes de proceder con despliegues.
Los reinicios son para actualizaciones del kernel y para el ocasional conflicto de controladores. Los conflictos de puertos merecen mejor trato: evidencia, una solución limpia y un futuro en el que no tengas que redescubrir la misma verdad a las 2 a.m.