Cómo encontrar el proceso con
ss, ver quién lo levantó y cortarlo sinkill -9. Y qué significa que esté escuchando en0.0.0.0.
Pasa así: levantás tu app, la terminal te devuelve EADDRINUSE y no arranca. Buscás el error y la primera respuesta dice kill -9 $(lsof -t -i:3000). Lo corrés y anda.
Casi siempre. A veces el puerto vuelve a estar ocupado un segundo después. Otras, lo que mataste era la base de otro proyecto que tenías abierto. Y en el camino se pasa por alto algo que importa más que el error: ese proceso que no sabías que seguía corriendo también podía estar atendiendo a otras máquinas.
Quién tiene el puerto
El error solo dice el puerto. Para saber quién lo tiene está ss, que viene en cualquier distro actual:
$ node -e "require('http').createServer().listen(3000)"
Error: listen EADDRINUSE: address already in use :::3000
$ ss -ltnp 'sport = :3000'
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 5 0.0.0.0:3000 0.0.0.0:* users:(("python3",pid=3365731,fd=3))
-l muestra lo que está escuchando, -t TCP, -n números en vez de nombres y -p el proceso. La última columna es la respuesta: un python3 con PID 3365731. Si esa columna sale vacía, el proceso es de otro usuario y con sudo adelante aparece.
Con el PID, ps te dice qué es y hace cuánto corre:
$ ps -o pid,ppid,user,etime,cmd -p 3365731
PID PPID USER ELAPSED CMD
3365731 3365724 valen 00:01 python3 -m http.server 3000
Acá dice un segundo porque lo levanté para el ejemplo; uno olvidado suele decir días. python3 -m http.server es un clásico: lo usaste para pasar un archivo y quedó corriendo en otra terminal, sirviendo la carpeta entera donde lo levantaste, archivos ocultos incluidos.
En macOS
ssno existe.lsof -i :3000te da lo mismo, y en Linux también anda.
Qué quiere decir 0.0.0.0
Volvé a la columna Local Address. 0.0.0.0:3000 quiere decir que el proceso acepta conexiones por cualquier interfaz de la máquina: le puede hablar cualquiera que esté en tu red.
En el artículo original esto es una figura donde mandás pedidos y cambiás la dirección mientras llegan. Con 0.0.0.0, cualquiera en tu red llega al puerto y se baja el .env. Con 127.0.0.1, solo tu navegador:
tu máquina: python3 -m http.server sirve .env y app.js
escucha en 0.0.0.0:3000
tu navegador recibe la respuesta
otra máquina de tu red se lleva el .env (OPENAI_API_KEY=sk-…)
internet lo corta el router
escucha en 127.0.0.1:3000
tu navegador recibe la respuesta
otra máquina de tu red conexión rechazada
internet lo corta el router
Red de ejemplo. El router corta lo que entra de internet mientras nadie le abra el puerto.
Lo probé con ese mismo servidor, levantado en una carpeta de prueba con un .env. Desde otra máquina de la red:
$ curl -s http://<ip>:3000/.env
OPENAI_API_KEY=sk-proj-demo-no-es-real
La key es de mentira y <ip> reemplaza la de mi máquina. Levantado con --bind 127.0.0.1, el mismo curl devuelve Couldn't connect to server. Y si lo corrés en tu home, la carpeta que se sirve incluye .ssh.
El error de Node de más arriba también lo dice: :::3000. Si no le pasás una dirección, listen(3000) escucha en ::, que son todas.
Un firewall o el router pueden cortar la conexión antes. En una red que no es tuya, no contaría con eso.
kill manda una señal
kill no mata: le manda una señal al proceso. Si no le decís cuál, manda TERM, que le pide terminar y lo deja cerrar lo que tiene abierto. kill -9 manda KILL, que el programa no puede atender: el kernel lo saca en el acto.
En el artículo original hay una figura con un programa que atiende pedidos y cada tanto guarda en el disco. Le mandás una señal, lo levantás de nuevo y probás la otra. Esto es lo que pasa con cada una:
# app (PID 4321) atiende pedidos, cada tanto guarda en datos.db y tiene su lock en app.pid
$ kill 4321 # TERM
# deja de aceptar pedidos nuevos y termina los que tenía
# guarda todo en datos.db y borra app.pid
# lo que llega después rebota: no hay nadie escuchando
$ kill -9 4321 # KILL
# el kernel lo saca en el acto
# el pedido a medio atender se corta y los cambios que tenía en memoria se pierden
# app.pid queda, y sigue diciendo que ya está corriendo (PID 4321)
Programa de ejemplo. No todos dejan un lock, pero cualquiera pierde lo que no alcanzó a escribir.
| Señal | Número | Qué pide | ¿La puede atender el proceso? |
|---|---|---|---|
HUP |
1 | Varios servicios la usan para releer la configuración | Sí |
INT |
2 | Es lo que manda Ctrl+C | Sí |
TERM |
15 | Que termine: cerrar lo que tiene abierto y salir | Sí |
KILL |
9 | Nada. El kernel lo saca en el momento | No |
El orden es kill, esperar unos segundos, y kill -9 solo si sigue ahí.
Si vuelve solo, es un servicio
A veces matás el proceso y el puerto vuelve a estar ocupado al segundo. Lo reproduje con un servicio de systemd que se reinicia solo, y pasó exactamente eso.
En el artículo original hay una figura donde le mandás pedidos, lo matás y mirás qué pasa con los que llegan mientras no está:
# systemd --user vigila demo-web.service (Restart=always)
# python3 -m http.server, con PID 3366803, tiene el puerto :3000
$ kill -9 3366803
# el proceso murió: lo que llega rebota hasta que systemd lo levanta
# un segundo después hay otro PID, 3366835, y el puerto vuelve a contestar
$ systemctl --user stop demo-web
# systemctl stop le avisa a systemd: nadie lo relanza y todo lo que llega rebota
Los dos PID son los de la prueba real.
El padre de ese proceso es systemd, así que matarlo no sirve. systemctl status acepta un PID y te dice de qué servicio es:
$ systemctl --user status 3366835 | head -4
● demo-web.service - [systemd-run] /usr/bin/python3 -m http.server 3000 --bind 127.0.0.1
Loaded: loaded (/run/user/1000/systemd/transient/demo-web.service; transient)
Transient: yes
Active: active (running) since Fri 2026-09-25 11:21:28 -03; 1s ago
Con eso sabés qué parar: systemctl --user stop demo-web. En un servidor lo normal es un servicio del sistema, y va sin --user. systemctl disable hace que no vuelva a arrancar con la máquina.
Si en la columna del proceso dice
docker-proxy, el puerto lo publicó un contenedor.docker pste dice cuál, y se para condocker stop.
El criterio
Antes de matar un proceso, fijate qué es y quién lo levantó: ss -ltnp y ps te lo dicen en dos líneas. Si vuelve solo, es un servicio y se para con systemctl stop.
Y mirá la dirección en la que escucha. Si dice 0.0.0.0 y no sabés por qué, arreglá eso antes que el error.
Practicalo en el lab: Del proceso al socket: una investigación con datos de juguete
Lo publiqué primero en vtsecurity.com.ar, donde las figuras son interactivas.
Top comments (1)
Agregaría una excepción a la línea del firewall: si el puerto lo publica Docker, ufw no lo corta. Con docker run -p 3000:3000 el puerto queda en todas las interfaces, y Docker desvía ese tráfico en la tabla nat antes de que llegue a las cadenas INPUT donde ufw aplica sus reglas. La documentación de Docker lo dice tal cual en la sección "Docker and ufw". Así que en el caso de docker-proxy que mencionas, un ufw deny 3000 no protege el .env aunque el comando no dé error.
El arreglo es el mismo que hiciste con --bind, pero en la publicación: -p 127.0.0.1:3000:3000, o "127.0.0.1:3000:3000" en compose. docker ps lo muestra en la columna PORTS. Si ves 0.0.0.0:3000->3000/tcp, el contenedor está expuesto a la red aunque el firewall diga lo contrario.