Guía9 min

Init y tini en el contenedor del agente IA: PID 1 que reenvía señales

Resumen

Cómo no dejar zombies ni tragarte el SIGTERM: init true en Compose, docker run --init (tini), ENTRYPOINT con -- en el Dockerfile. Node como PID 1 no recolecta hijos de tools. Fly no trae tini. Un init corto, no un while true.

Docker
Proceso init reenviando SIGTERM al runtime del agente y recolectando zombies

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

El tool spawnó un ffmpeg y se colgó. El hijo quedó zombie. Node, como PID 1, no reaps. A las 6 h el kernel se queda sin PIDs y el webhook deja de aceptar. O al revés: docker stop manda SIGTERM a un shell wrapper y Node nunca se entera; a los 10 s llega SIGKILL a mitad de un sendMessage. Graceful shutdown cubre el handler. Esta guía cubre quién es PID 1. Fuentes oficiales consultadas el 3 de septiembre de 2026.

La regla

Prod VPS / Compose: init: true (o docker run --init). El binario que Docker mete como PID 1 es docker-init, respaldado por tini. Reenvía señales y recolecta zombies.

Si empaquetas tini en la imagen: ENTRYPOINT ["/sbin/tini", "--"] + CMD ["node","server.js"]. El -- no es decoración: sin él, flags del hijo se las come tini (tini: invalid option -- 'c').

No uses while true; do node; done como init. Eso pelea con restart: el orquestador no ve el exit code.

Workers / Vercel Functions no tienen PID 1. No copies tini ahí.

Qué hace un init (y por qué Node no basta)

Linux entrega a PID 1 dos trabajos que un runtime de agente no hace bien:

  1. Reap. Un hijo huérfano se reparenta a PID 1. Si PID 1 no llama wait(), el zombie se queda. Un agente que lanza tools (bash, git, python, browsers) fabrica huérfanos todo el día.
  2. Señales. El kernel trata a PID 1 distinto: handlers default no siempre matan. Tini documenta el síntoma: sin init, SIGTERM no termina el proceso aunque “debería”. Con tini, el default vuelve a funcionar aunque no hayas instalado un listener.

Node puede escuchar SIGTERM. Eso no lo convierte en init. No reaps a los nietos del tool. El handler de graceful y tini se complementan: tini entrega la señal; Node cierra el server.

Docs de Node (hoy): si no hay listener, SIGTERM/SIGINT salen con código 128 + número de señal (no-Windows). Si instalas listener, se quita el default: si el handler no hace process.exit, el proceso se queda vivo y Docker espera el timeout.

Compose: init: true

Compose (referencia de services, consultada hoy): init corre un proceso init como PID 1 dentro del contenedor. Reenvía señales y reaps. El valor es true. Ejemplo canónico: servicio web con image: alpine:latest y init: true. El binario es específico de plataforma — no asumas un path dentro de la imagen.

Eso es lo más barato: no metes tini en el Dockerfile, no pinneas una release, no pelea con USER no-root. Docker lo inyecta al crear el contenedor.

Si el agente corre con docker compose up en un VPS, esta línea basta. El init no cambia la restart policy; solo hace de PID 1.

docker run --init

CLI de docker container run (hoy): --init (API 1.25+) corre un init dentro del contenedor que reenvía señales y reaps. El default es el primer ejecutable docker-init en el PATH del daemon, no del contenedor. La instalación por defecto de Docker lo incluye; está respaldado por tini.

Úsalo en el mismo sitio donde pones --restart unless-stopped y --user. Un one-liner de debug sin --init no prueba el comportamiento de prod.

No es un flag de build. docker build no lo hereda. Si el runtime de Fly/Railway no pasa --init, la imagen tiene que traer tini en el ENTRYPOINT o el proceso 1 será Node otra vez.

Empaquetar tini en la imagen

README de krallin/tini (hoy): tini spawnea un hijo, espera a que salga, reaps zombies y reenvía señales. Transparente: una imagen que funcionaba sin tini sigue funcionando con tini.

Versión citada en su guía de instalación: v0.19.0. Alpine: apk add --no-cache tini/sbin/tini. Debian Buster o más nuevo: paquete tini/usr/bin/tini (no /tini). Binarios firmados con la clave 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7. Hay checksums SHA1/SHA256. Preferí el paquete de la distro en el stage runner (multi-stage); un ADD de GitHub en el runner es superficie extra.

Forma exec, no shell:

USER node
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]

ENTRYPOINT tini -- node (sin JSON) vuelve a un shell y el -- se pierde. CMD npm start pone npm debajo de tini: npm a veces se come SIGTERM. No-root ya lo marca: PID 1 efectivo para Node tiene que ser node, con tini arriba.

Si ves tini: invalid option -- 'c', falta el -- antes del CMD. Flags de tini van antes: /tini -v -- /your/program. En un Dockerfile normal no hace falta -s (subreaper); tini avisa si no es PID 1.

Fly, Railway, serverless

Fly (fly.toml, hoy) no documenta un equivalente a init: true. Manda SIGINT por defecto al apagar; kill_signal permite SIGTERM/SIGQUIT/SIGUSR1/SIGUSR2/SIGKILL/SIGSTOP. kill_timeout default 5 s, máximo 300 s, best-effort: el app tiene que tolerar un corte más corto. Si el ENTRYPOINT es tini y el handler de Node solo escucha SIGTERM, alinea kill_signal = "SIGTERM" o escucha también SIGINT.

Railway reemplaza el proceso en el deploy. Si construyes con Dockerfile, el ENTRYPOINT de tini aplica. Nixpacks/node dist/server.js sin init deja a Node como PID 1.

Vercel Functions y Cloudflare Workers: isolate por request. No hay init, no hay zombies de contenedor. Un child_process ahí es otra historia (y suele estar prohibido o limitado).

Tabla

Init inyectado versus Node como PID 1

RuntimePID 1 (docs 2026-09-03)Reap / señales
Compose init: truedocker-init (tini)sí, binario del platform
docker run --initprimer docker-init del daemon
Imagen con /sbin/tini --tinisí, portable a Fly
CMD ["node",…] soloNodehandler sí; reap de nietos no
CMD npm startnpmSIGTERM a menudo perdido
sh -c "node …"shellseñal no llega a Node
Fly Machinesel ENTRYPOINTsin --init de Docker; empaca tini
Workers / FunctionsN/Ano hay PID 1

Errores comunes

Zombies acumulados y SIGTERM que no apaga el bot

SíntomaCausaFix
defunct subeNode PID 1, tool huérfanoinit: true o tini
docker stop tarda 10 sshell/npm PID 1tini + CMD ["node",…]
tini: invalid option -- 'c'falta --ENTRYPOINT ["/sbin/tini","--"]
Fly mata a los 5 sSIGINT default, handler solo TERMkill_signal o escucha INT
Loop de arranqueswhile true + restartquita el while; restart
Warning “not PID 1”tini debajo de otro initun solo init; no apiles

Relación con el resto

Checklist

  • Compose init: true o tini en ENTRYPOINT, no los dos apilados sin motivo
  • CMD ["node","server.js"] forma exec
  • -- detrás de tini
  • Handler SIGTERM y SIGINT si Fly sigue en default
  • No while true en el entrypoint
  • Ensayo: docker stop baja en menos del timeout; ps dentro no llena de defunct
  • Ensayo Fly: SIGINT/SIGTERM llega a Node (log de una línea)

FAQ

¿Tini y init: true juntos? Docker inyecta docker-init como PID 1. Si tu ENTRYPOINT ya es tini, queda tini como hijo. Un init basta. En Compose local usa init: true y ENTRYPOINT node; en Fly empaca tini porque no hay --init.

¿dumb-init? Misma idea, otro binario. Docker documenta tini vía docker-init. No mezcles tres inits.

¿systemd en el contenedor? No. Un agente no es una distro. tini es el init de un proceso.

¿Healthcheck como PID 1? No. HEALTHCHECK es un probe periódico, no reaps.

Ensayo

docker compose run --rm --init agente ps aux: PID 1 = docker-init/tini. docker stop -t 15: el log del handler aparece antes del kill. Un defunct en prod cierra el debate.

Siguiente paso: si PID 1 ya es tini y el stop sigue siendo SIGKILL, el handler no sale a tiempo — graceful y sube stop_grace_period, no agregues otro init. Sin runtime: curso.