La lección anterior terminó con una pregunta incómoda: aurora-db y aurora-cache llevan minutos corriendo tan tranquilos, pero aurora-api murió a los dos segundos con código de salida 1. Y si pruebas docker run -d ubuntu:24.04, verás algo aún más desconcertante: el contenedor se para solo, sin error, sin log y sin que nadie lo toque. No es un fallo de Docker; es la consecuencia directa de la regla más importante de todo el módulo: un contenedor vive exactamente lo que viva su proceso PID 1. Ni un segundo más.

En esta lección vas a recorrer los siete estados por los que puede pasar un contenedor y las transiciones entre ellos, vas a entender qué ocurre de verdad cuando escribes docker stop —la secuencia SIGTERM, periodo de gracia, SIGKILL—, vas a medir con un cronómetro por qué la forma shell del CMD cuesta siempre diez segundos, y vas a aprender a leer los códigos de salida: ese 1, y también el 125, el 137 y el 143, que dejan de ser números crípticos para convertirse en diagnósticos. Y le enseñarás a server.js a apagarse como es debido, cerrando el pool de PostgreSQL y el cliente de Redis antes de irse.

Contenido

  1. Los estados de un contenedor
  2. El PID 1 y la regla fundamental
  3. Por qué ubuntu muere y nginx no
  4. start, stop y restart
  5. pause y unpause: congelar sin matar
  6. kill y el envío de señales concretas
  7. Parada ordenada frente a parada forzada
  8. La forma shell del CMD y sus diez segundos, medidos
  9. Códigos de salida y su tabla de diagnóstico
  10. Apagado ordenado de aurora-api
  11. docker wait y docker rename

  1. Los estados de un contenedor

Un contenedor no está simplemente "encendido" o "apagado". Docker maneja siete estados:

stateDiagram-v2
    [*] --> created: docker create<br/>(o la 1ª mitad de docker run)
    created --> running: docker start
    running --> paused: docker pause
    paused --> running: docker unpause
    running --> exited: el PID 1 termina<br/>docker stop / docker kill
    running --> restarting: política de reinicio<br/>(lección 03-07)
    restarting --> running: reintento correcto
    restarting --> exited: se agotan los reintentos
    exited --> running: docker start
    exited --> removing: docker rm
    paused --> exited: docker stop
    removing --> [*]: contenedor eliminado
    running --> dead: fallo del demonio<br/>o del sistema de ficheros
    dead --> removing: docker rm -f

Y la tabla que necesitas tener a mano:

Estado Cómo se llega ¿Hay proceso? ¿Consume RAM/CPU? Qué se conserva
created docker create No No Capa de escritura vacía y configuración
running docker start, docker run, unpause Todo
paused docker pause Sí, congelado RAM sí, CPU no Todo, incluida la memoria del proceso
restarting Política --restart tras un fallo Transitoriamente no Poco Todo
exited El PID 1 termina, docker stop, docker kill No No Capa de escritura, logs y configuración
dead El demonio no pudo eliminarlo correctamente No No Restos; solo se puede borrar
removing docker rm en curso No No Nada, es transitorio

Los dos estados que más se confunden son exited y removing, y la diferencia es enorme:

  • Un contenedor exited sigue existiendo. Ocupa su nombre, conserva su capa de escritura con todos los ficheros que escribió, guarda sus logs y su configuración, y puedes volver a arrancarlo con docker start. Es un contenedor dormido, no un contenedor borrado.
  • docker rm es lo que lo elimina de verdad, y con él se van la capa de escritura y los logs para siempre.

Consulta el estado de cualquier contenedor con precisión:

docker inspect --format '{{.State.Status}}' aurora-db
docker inspect --format 'Estado: {{.State.Status}} | PID: {{.State.Pid}} | Arrancado: {{.State.StartedAt}}' aurora-db
running
Estado: running | PID: 24817 | Arrancado: 2026-08-04T19:28:41.113927Z

Ese PID: 24817 es el identificador del proceso en tu máquina: los contenedores no son máquinas virtuales, son procesos del host aislados con namespaces, como viste en la lección 01-01.

  1. El PID 1 y la regla fundamental

Dentro de su namespace de procesos, el comando principal del contenedor se ve a sí mismo como PID 1. Compruébalo:

docker exec aurora-db ps -o pid,comm
PID   COMMAND
    1 postgres
   67 postgres
   68 postgres
  ...

PostgreSQL es el PID 1 dentro de aurora-db. Y de ahí sale la regla:

El contenedor existe mientras exista su PID 1. Cuando ese proceso termina —bien o mal, con error o sin él—, el contenedor pasa a exited. Da igual que dentro hubiera cien procesos hijos: todos mueren con él.

Ser PID 1 tiene dos implicaciones que se notan en el día a día:

Implicación Consecuencia práctica
El PID 1 no tiene manejadores de señales por defecto para SIGTERM en algunos lenguajes Un proceso que ignore SIGTERM se queda colgado hasta el SIGKILL
El PID 1 es responsable de recoger los procesos zombis Un PID 1 que no lo haga acumula zombis; se resuelve con --init

La opción --init inyecta un init mínimo (tini) como PID 1, que reenvía señales y recoge zombis:

docker run -d --init --name con-init nginx:alpine
docker exec con-init ps -o pid,comm | head -3
PID   COMMAND
    1 /sbin/docker-init
    7 nginx

Ahora nginx es el PID 7 y docker-init es el 1. Para aurora-api no hace falta: node es un PID 1 correcto siempre que uses la forma exec, como haces desde la lección 02-04.

docker rm -f con-init

  1. Por qué ubuntu muere y nginx no

Esta es la demostración que aclara de golpe el 80 % de los "mi contenedor se para solo":

docker run -d --name prueba-ubuntu ubuntu:24.04
docker run -d --name prueba-nginx nginx:alpine
sleep 2
docker ps -a --filter name=prueba- --format "table {{.Names}}\t{{.Status}}\t{{.Command}}"
NAMES            STATUS                     COMMAND
prueba-nginx     Up 2 seconds               "/docker-entrypoint.…"
prueba-ubuntu    Exited (0) 2 seconds ago   "/bin/bash"

La diferencia está en la columna COMMAND:

  • La imagen ubuntu:24.04 tiene CMD ["/bin/bash"]. Un bash sin terminal y sin entrada no tiene nada que leer: llega al final de su entrada estándar y termina correctamente, con código 0. El contenedor no ha fallado; ha hecho exactamente lo que le pediste, que era ejecutar un shell que no tenía nada que hacer.
  • La imagen nginx:alpine ejecuta nginx -g "daemon off;". Ese daemon off es la clave: obliga a Nginx a quedarse en primer plano en vez de convertirse en demonio de fondo. El proceso no termina nunca, así que el contenedor tampoco.

Haz que el de Ubuntu viva dándole algo que hacer:

docker rm prueba-ubuntu
docker run -d --name prueba-ubuntu ubuntu:24.04 sleep 60
docker ps --filter name=prueba-ubuntu --format "{{.Names}}: {{.Status}}"
prueba-ubuntu: Up 3 seconds

O dale una terminal, que es lo que hacías en la lección 01-06 con -it:

docker rm -f prueba-ubuntu
docker run -d -it --name prueba-ubuntu ubuntu:24.04
docker ps --filter name=prueba-ubuntu --format "{{.Names}}: {{.Status}}"
prueba-ubuntu: Up 2 seconds

Con -it, bash tiene un terminal abierto esperando entrada y no termina. De ahí sale el error clásico:

Un contenedor no es una máquina que enciendes: es un proceso que ejecutas. Si quieres que dure, dale un proceso que dure. Y el error inverso —"pongo daemon on en Nginx para que funcione como en mi servidor"— convierte el contenedor en un suicidio inmediato: Nginx se va al fondo, el proceso original termina y Docker da por acabado el contenedor.

Es también la explicación de lo que le pasó a aurora-api: su PID 1, node, hizo process.exit(1) al no poder conectar con Redis. El proceso terminó, así que el contenedor terminó.

docker rm -f prueba-ubuntu prueba-nginx

  1. start, stop y restart

docker stop aurora-cache
docker start aurora-cache
docker restart aurora-cache
aurora-cache
aurora-cache
aurora-cache
Comando Qué hace ¿Mismo contenedor? ¿Mismo PID?
docker stop SIGTERM, espera, SIGKILL
docker start Vuelve a lanzar el comando original , mismo ID y misma capa de escritura No, PID nuevo
docker restart stop + start en un solo paso No, PID nuevo

Lo importante es entender qué sobrevive a un restart y qué no, porque es una fuente constante de sorpresas:

docker exec aurora-cache redis-cli SET libro:favorito "Rayuela"
docker exec aurora-cache sh -c 'echo "nota temporal" > /tmp/nota.txt'
docker restart aurora-cache
sleep 2
docker exec aurora-cache cat /tmp/nota.txt
docker exec aurora-cache redis-cli GET libro:favorito
OK
aurora-cache
nota temporal
(nil)

Dos resultados opuestos en el mismo comando:

  • /tmp/nota.txt sigue ahí: está escrito en la capa de escritura del contenedor, que sobrevive a paradas y arranques. Solo desaparece con docker rm.
  • La clave de Redis ha desaparecido: vivía en la memoria del proceso, y el proceso es nuevo. Redis sin persistencia configurada pierde todo al reiniciarse.

Esa distinción entre "lo que está en disco dentro del contenedor" y "lo que está en la memoria del proceso" es imprescindible, y todavía falta una tercera categoría —"lo que está fuera del contenedor y sobrevive incluso a docker rm"—, que es el tema de la lección 03-06.

También se pueden operar varios contenedores a la vez:

docker stop aurora-db aurora-cache
docker start aurora-db aurora-cache

Un aviso sobre docker start: no acepta cambios de configuración. docker start -p 5433:5432 aurora-db no existe. Vuelve a arrancar el contenedor tal y como se creó, con sus puertos, variables y montajes originales. Es la consecuencia de la separación create/start de la lección anterior.

Y una opción útil de start:

docker start -a aurora-cache

-a (--attach) arranca el contenedor y engancha tu terminal a su salida, como si lo hubieras lanzado en primer plano. Sirve para ver el arranque de un servicio que sospechas que falla.

  1. pause y unpause: congelar sin matar

docker pause aurora-cache
docker ps --filter name=aurora-cache --format "{{.Names}}: {{.Status}}"
aurora-cache
aurora-cache: Up 8 minutes (Paused)

docker pause congela todos los procesos del contenedor usando el freezer de cgroups. Ni el proceso se entera: no recibe ninguna señal, simplemente deja de recibir tiempo de CPU. Y desde fuera:

docker exec aurora-cache redis-cli PING

Ese comando se queda colgado indefinidamente: el contenedor no puede responder porque no se está ejecutando. Púlsalo con Ctrl+C y descongélalo:

docker unpause aurora-cache
docker exec aurora-cache redis-cli PING
aurora-cache
PONG
pause stop
El proceso Congelado, sigue existiendo Terminado
La memoria del proceso Se conserva íntegra Se pierde
Señales enviadas Ninguna SIGTERM y, si hace falta, SIGKILL
Conexiones de red abiertas Se mantienen, pero sin respuesta (acaban expirando) Se cierran
Al reanudar Continúa exactamente donde estaba Arranca de cero
Consumo de RAM Sigue ocupada Liberada

Usos reales de pause: liberar CPU momentáneamente sin perder el estado de un proceso largo, congelar un contenedor mientras haces una copia de seguridad coherente de sus ficheros, o detener temporalmente una aplicación mientras diagnosticas otra. No sirve para ahorrar memoria: la RAM sigue ocupada.

  1. kill y el envío de señales concretas

docker kill aurora-cache
docker ps -a --filter name=aurora-cache --format "{{.Names}}: {{.Status}}"
docker start aurora-cache
aurora-cache
aurora-cache: Exited (137) 3 seconds ago
aurora-cache

docker kill envía SIGKILL de inmediato, sin periodo de gracia. El proceso no puede capturarla, ignorarla ni negociar: el kernel lo termina en el acto. Por eso el código de salida es 137, del que hablaremos en el apartado 9.

Pero docker kill sirve para mucho más que matar, porque envía cualquier señal:

docker kill -s SIGHUP <contenedor>
Señal Uso típico en contenedores
SIGTERM (15) Petición cortés de terminar. Es la que envía docker stop
SIGKILL (9) Terminación inmediata e incapturable. La que envía docker kill por defecto
SIGHUP (1) Recargar configuración sin reiniciar (Nginx, HAProxy)
SIGQUIT (3) Apagado ordenado en Nginx; volcado de hilos en la JVM
SIGUSR1/SIGUSR2 (10/12) Señales a medida: rotar logs en Nginx, volcar el heap en Node
SIGINT (2) Lo que envía Ctrl+C

Un ejemplo real: recargar la configuración de Nginx sin cortar ni una conexión.

docker run -d --name recarga-demo -p 8080:80 nginx:alpine
docker kill -s SIGHUP recarga-demo
docker ps --filter name=recarga-demo --format "{{.Names}}: {{.Status}}"
recarga-demo: Up 20 seconds

Sigue Up: la señal no lo ha matado, Nginx la ha interpretado como "relee tu configuración". Lo usarás en la lección 03-05 cuando montes aurora-web como proxy inverso.

docker rm -f recarga-demo

  1. Parada ordenada frente a parada forzada

Este es el apartado central de la lección. docker stop no mata el contenedor de golpe: ejecuta una secuencia de tres pasos.

sequenceDiagram
    participant U as Tú
    participant D as dockerd
    participant P as PID 1 del contenedor
    U->>D: docker stop aurora-api
    D->>P: 1. SIGTERM (o el STOPSIGNAL de la imagen)
    Note over P: Periodo de gracia: 10 s por defecto
    alt El proceso termina a tiempo
        P-->>D: Cierra conexiones, libera recursos y sale
        D-->>U: Contenedor exited con su código
    else El proceso no responde
        Note over D,P: Se agotan los 10 s
        D->>P: 2. SIGKILL (incapturable)
        P-->>D: Terminación abrupta, código 137
        D-->>U: Contenedor exited (137)
    end

Los tres pasos son:

  1. Docker envía el STOPSIGNAL de la imagen (por defecto SIGTERM) al PID 1. Recuerda de la lección 02-04 que Nginx declara STOPSIGNAL SIGQUIT.
  2. Espera el periodo de gracia: 10 segundos por defecto.
  3. Si el proceso sigue vivo, envía SIGKILL.

El periodo de gracia se ajusta con --time (o -t):

docker stop --time 30 aurora-db     # espera hasta 30 segundos
docker stop --time 0 aurora-cache   # SIGKILL inmediato, equivale a docker kill

¿Cuándo conviene subirlo? Cuando el proceso necesita tiempo para cerrar bien: una base de datos volcando su búfer a disco, un trabajador terminando la tarea que tenía entre manos, una API esperando a que se completen las peticiones en curso. Para PostgreSQL, 30 segundos es una cifra razonable; con 10 podrías forzar una recuperación al arrancar de nuevo.

docker stop docker kill
Señal inicial STOPSIGNAL de la imagen (SIGTERM) SIGKILL (o la de -s)
¿Puede capturarse? No
Periodo de gracia 10 s por defecto, ajustable con -t Ninguno
Datos en búfer El proceso puede volcarlos Se pierden
Código de salida típico 0 o 143 137
Cuándo usarlo Siempre por defecto Solo si el proceso no responde

Y el resumen que conviene memorizar: stop pide, kill obliga. Un docker kill sobre PostgreSQL es el equivalente a desenchufar el servidor.

  1. La forma shell del CMD y sus diez segundos, medidos

Aquí llega la factura de una decisión que parecía cosmética en la lección 02-03. Vamos a medirla con un cronómetro.

Crea una imagen de demostración que use la forma shell:

# ~/aurora-libros/api/Dockerfile.shell — SOLO para esta demostración
FROM auroralibros/aurora-api:1.1.0
ENTRYPOINT []
CMD echo "[arranque] iniciando aurora-api" && node server.js
docker build -f ~/aurora-libros/api/Dockerfile.shell -t aurora-api:forma-shell ~/aurora-libros/api

Ahora arranca las dos versiones. Como la API todavía no encuentra sus dependencias, usaremos Nginx para que la comparación sea limpia y reproducible sin depender de nada:

# Forma exec: nginx es PID 1 y recibe la señal directamente
docker run -d --name parada-exec nginx:alpine

# Forma shell: sh es PID 1 y nginx es su hijo
docker run -d --name parada-shell nginx:alpine \
  sh -c 'echo "[arranque] iniciando nginx" && nginx -g "daemon off;"'

docker exec parada-exec ps -o pid,comm | head -3
docker exec parada-shell ps -o pid,comm | head -3
PID   COMMAND
    1 nginx
   30 nginx

PID   COMMAND
    1 sh
    7 nginx
   14 nginx

Ahí está la diferencia, visible en una sola línea: en el primero nginx es el PID 1; en el segundo, el PID 1 es sh y nginx es un hijo suyo. Cronometra las paradas:

time docker stop parada-exec
time docker stop parada-shell
parada-exec
real    0m0.128s

parada-shell
real    0m10.271s

Ochenta veces más lento. Y los códigos de salida cuentan el resto de la historia:

docker inspect --format '{{.Name}} → código {{.State.ExitCode}}' parada-exec parada-shell
/parada-exec → código 0
/parada-shell → código 137

Reconstruyamos lo ocurrido en el segundo caso:

  1. docker stop envía SIGQUIT (el STOPSIGNAL de Nginx) al PID 1, que es sh.
  2. sh no tiene ningún manejador para esa señal ni sabe nada de reenviarla a sus hijos. La ignora o termina él solo, pero nginx no se entera de nada.
  3. Docker espera sus diez segundos completos.
  4. SIGKILL. Todo el contenedor muere de golpe, con las conexiones cortadas a media petición. Código 137.

Un matiz honesto que casi nadie menciona: algunos shells optimizan el caso más simple. Si el CMD fuera exactamente CMD node server.js, muchos shells (incluidos dash y el ash de BusyBox) reemplazan su propio proceso por node mediante exec, y el problema no aparece. Pero basta con que haya dos comandos encadenados con &&, una redirección o una tubería —como en el ejemplo, y como en el 90 % de los CMD reales del mundo— para que el shell tenga que quedarse como PID 1 y el fallo se manifieste. No merece la pena jugarse el comportamiento de parada a una optimización del shell:

Usa siempre la forma exec: CMD ["node", "server.js"]. Si necesitas la lógica de un shell, escríbela en un docker-entrypoint.sh que termine en exec "$@", como hiciste en la lección 02-04.

Limpia la demostración:

docker rm parada-exec parada-shell
docker image rm aurora-api:forma-shell

  1. Códigos de salida y su tabla de diagnóstico

Cuando un contenedor termina, deja un número. Ese número es la primera pista de cualquier investigación.

docker ps -a --filter name=aurora --format "table {{.Names}}\t{{.Status}}"
docker inspect --format '{{.State.ExitCode}}' aurora-api
NAMES          STATUS
aurora-cache   Up 4 minutes
aurora-db      Up 12 minutes
1

La tabla que resuelve la mayoría de los casos:

Código Significado Causa habitual en la práctica
0 Terminación correcta El proceso hizo su trabajo y salió. También bash sin entrada
1 Error genérico de la aplicación Excepción no capturada, process.exit(1), configuración inválida
125 Fallo del propio docker run Opción mal escrita, --env-file inexistente, nombre duplicado. El contenedor ni se creó
126 El comando existe pero no se pudo ejecutar Falta el bit de ejecución (chmod +x), o se intentó ejecutar un directorio
127 Comando no encontrado Errata en el CMD, o binario ausente en una imagen mínima
137 128 + 9 → SIGKILL docker kill, agotado el plazo de docker stop, o el OOM killer (lección 03-07)
139 128 + 11 → SIGSEGV Violación de segmento: bug en código nativo o binario incompatible con la arquitectura
143 128 + 15 → SIGTERM El proceso terminó por SIGTERM sin manejador propio. Es una parada normal

La regla que explica la mitad de la tabla: si el código es mayor que 128, el proceso murió por una señal, y el número de la señal es código − 128. 137 − 128 = 9 (SIGKILL); 143 − 128 = 15 (SIGTERM); 139 − 128 = 11 (SIGSEGV).

Y la distinción más importante para depurar, que separa dos mundos:

Rango Quién falló Dónde buscar
125, 126, 127 Docker o el arranque del comando En tu línea de comandos y en el Dockerfile
1, 2, … , 124 Tu aplicación En docker logs
>128 Una señal externa o del kernel En docker inspect (OOMKilled, Error)

Vamos a provocarlos para verlos de verdad:

docker run --name codigo-0 alpine:3.20 true
docker run --name codigo-1 alpine:3.20 sh -c 'exit 1'
docker run --name codigo-127 alpine:3.20 comando-que-no-existe
docker run --opcion-inventada alpine:3.20
docker run --name codigo-126 alpine:3.20 /etc
docker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: exec: "comando-que-no-existe": executable file not found in $PATH

unknown flag: --opcion-inventada

docker: Error response from daemon: failed to create task for container:
failed to create shim task: OCI runtime create failed: exec: "/etc": permission denied
docker ps -a --filter name=codigo- --format "table {{.Names}}\t{{.Status}}"
echo "Salida del docker run inválido: $?"
NAMES        STATUS
codigo-126   Created
codigo-127   Created
codigo-1     Exited (1) 8 seconds ago
codigo-0     Exited (0) 12 seconds ago
125

Observa el detalle revelador: codigo-127 y codigo-126 se quedaron en estado Created, no Exited. El contenedor llegó a crearse pero nunca llegó a arrancar, porque el ejecutable no existía o no se podía ejecutar. En cambio, codigo-1 sí arrancó y su aplicación decidió salir con error. Es la diferencia entre "no pudo empezar" y "empezó y falló", y te ahorra buscar en el sitio equivocado.

docker rm codigo-0 codigo-1 codigo-126 codigo-127

Aplicado a Aurora Libros, el Exited (1) de la lección anterior queda diagnosticado sin ambigüedad: la aplicación arrancó y falló ella misma, no fue Docker ni una señal. Los logs confirman el motivo (getaddrinfo ENOTFOUND aurora-cache) y el process.exit(1) de server.js explica el número exacto.

  1. Apagado ordenado de aurora-api

Ha llegado el momento de arreglar dos cosas a la vez en server.js: que el proceso no muera porque una dependencia opcional no esté disponible, y que cuando le pidan terminar lo haga cerrando lo que tiene abierto.

Sustituye el bloque final de ~/aurora-libros/api/server.js (el que empieza en // --- Arranque ---) por este:

// --- Arranque ---
let servidor;

async function arrancar() {
  // La caché es una dependencia OPCIONAL: si Redis no responde, la API debe
  // seguir sirviendo el catálogo desde PostgreSQL, no morir en el arranque.
  cache.connect().catch((err) =>
    console.error('[cache] no disponible al arrancar:', err.message)
  );

  servidor = app.listen(PORT, '0.0.0.0', () => {
    console.log(`[aurora-api] escuchando en el puerto ${PORT}`);
    console.log(`[aurora-api] base de datos: ${DB_HOST}:${DB_PORT}/${DB_NAME}`);
    console.log(`[aurora-api] caché: ${REDIS_HOST}:${REDIS_PORT}`);
  });
}

// --- Apagado ordenado ---
let apagando = false;

async function apagar(senal) {
  if (apagando) return;          // Un segundo Ctrl+C no debe reentrar aquí
  apagando = true;
  console.log(`[aurora-api] recibida ${senal}, cerrando ordenadamente...`);

  // Red de seguridad: salir por nuestro pie ANTES de que llegue el SIGKILL.
  // 8 segundos < los 10 del periodo de gracia de docker stop.
  const forzar = setTimeout(() => {
    console.error('[aurora-api] el cierre se ha atascado, salida forzada');
    process.exit(1);
  }, 8000);
  forzar.unref();

  servidor.close(async () => {
    try {
      await pool.end();
      console.log('[aurora-api] pool de PostgreSQL cerrado');
      if (cache.isOpen) {
        await cache.quit();
        console.log('[aurora-api] cliente de Redis cerrado');
      }
    } catch (err) {
      console.error('[aurora-api] error durante el cierre:', err.message);
    }
    console.log('[aurora-api] cerrado limpiamente');
    process.exit(0);
  });
}

process.on('SIGTERM', () => apagar('SIGTERM'));
process.on('SIGINT', () => apagar('SIGINT'));

arrancar().catch((err) => {
  console.error('[aurora-api] fallo al arrancar:', err.message);
  process.exit(1);
});

Repasemos las decisiones una por una, porque cada una responde a algo que has aprendido en esta lección:

Línea Por qué está ahí
cache.connect().catch(...) sin await El arranque no se bloquea por una dependencia opcional. El contenedor se queda running y podrás estudiarlo, en lugar de morir con código 1
servidor = app.listen(...) guardado en una variable Sin la referencia al servidor no se puede llamar a .close()
if (apagando) return; Evita que dos señales seguidas lancen dos cierres simultáneos
setTimeout(..., 8000) 8 < 10: si algo se atasca, salimos nosotros antes del SIGKILL y con un código nuestro, no con un 137
.unref() Impide que ese temporizador mantenga vivo el proceso si el cierre va bien
servidor.close(callback) Deja de aceptar conexiones nuevas y espera a que terminen las en curso
await pool.end() Cierra las conexiones a PostgreSQL. Sin esto, la base de datos las mantiene abiertas hasta que expiren
if (cache.isOpen) quit() sobre un cliente que nunca conectó lanzaría una excepción
process.exit(0) Salida correcta y explícita: código 0, no 143
SIGINT además de SIGTERM Para que Ctrl+C en primer plano se comporte igual que docker stop

Construye la nueva versión. Al cambiar el comportamiento sin romper la API, sube el número MENOR según lo que decidiste en la lección 02-06:

docker build -t auroralibros/aurora-api:1.2.0 \
             -t auroralibros/aurora-api:1.2 \
             --build-arg VERSION=1.2.0 \
             ~/aurora-libros/api

Y ahora la comprobación cronometrada. Compara la versión antigua con la nueva:

# Versión 1.1.0: moría al arrancar. Con la 1.2.0 el contenedor se mantiene vivo.
docker run -d --name api-parada --env-file ~/aurora-libros/aurora.env \
  auroralibros/aurora-api:1.2.0
sleep 3
docker ps --filter name=api-parada --format "{{.Names}}: {{.Status}}"
api-parada: Up 3 seconds (health: starting)

El contenedor sobrevive. Ya no muere por no encontrar Redis; simplemente lo registra y sigue. Ahora párala midiendo el tiempo:

time docker stop api-parada
docker logs --tail 6 api-parada
docker inspect --format 'Código de salida: {{.State.ExitCode}}' api-parada
api-parada
real    0m0.187s

[cache] no disponible al arrancar: getaddrinfo ENOTFOUND aurora-cache
[aurora-api] escuchando en el puerto 3000
[aurora-api] base de datos: aurora-db:5432/aurora_libros
[aurora-api] recibida SIGTERM, cerrando ordenadamente...
[aurora-api] pool de PostgreSQL cerrado
[aurora-api] cerrado limpiamente
Código de salida: 0

Las tres cosas que buscábamos, confirmadas en una sola salida:

  • 0,187 segundos, frente a los 10,271 de la forma shell. La señal llegó a node porque es PID 1 y la forma exec la deja pasar.
  • Los logs cuentan el cierre: el pool de PostgreSQL se cerró explícitamente antes de salir. El cliente de Redis no aparece porque nunca llegó a abrirse (cache.isOpen era falso), exactamente como preveía el código.
  • Código de salida 0, no 143. La diferencia entre "el proceso fue terminado por una señal" y "el proceso decidió terminar correctamente porque se lo pidieron bien".

En un orquestador esta diferencia es la que separa un despliegue sin errores de uno con peticiones cortadas a mitad y conexiones huérfanas en la base de datos.

docker rm api-parada

  1. docker wait y docker rename

Dos comandos pequeños que resuelven problemas concretos.

docker wait

Bloquea hasta que el contenedor termine e imprime su código de salida:

docker run -d --name tarea-lenta alpine:3.20 sh -c 'sleep 5; exit 3'
docker wait tarea-lenta
docker rm tarea-lenta
7d3f9a1b2c48
3

Se queda cinco segundos parado y después imprime 3. Es la pieza que permite encadenar en un script: "espera a que termine la migración de la base de datos y sigue solo si fue bien".

docker run -d --name migracion alpine:3.20 sh -c 'echo "migrando..."; sleep 3'
if [ "$(docker wait migracion)" -eq 0 ]; then
  echo "Migración correcta, arrancando la API"
else
  echo "La migración falló, abortando el despliegue"
fi
docker rm migracion
Migración correcta, arrancando la API

docker rename

Cambia el nombre de un contenedor en caliente, sin pararlo ni recrearlo:

docker rename aurora-cache aurora-cache-viejo
docker ps --format "{{.Names}}: {{.Status}}" --filter name=aurora
docker rename aurora-cache-viejo aurora-cache
aurora-cache-viejo: Up 25 minutes
aurora-db: Up 33 minutes

Es más útil de lo que parece: en un despliegue sin corte, renombras el contenedor antiguo a aurora-api-viejo, arrancas el nuevo con el nombre bueno y borras el antiguo cuando confirmas que todo va bien. Y un aviso que enlaza con la lección 03-05: si otro contenedor lo estaba resolviendo por DNS con su nombre anterior, dejará de encontrarlo en cuanto lo renombres.

Errores Comunes y Consejos

  • Creer que un contenedor Exited está borrado. Sigue existiendo, ocupa su nombre y conserva capa de escritura y logs. Se borra con docker rm, no con docker stop.
  • Poner un servicio en modo demonio dentro del contenedor. nginx sin daemon off, httpd -k start, postgres con pg_ctl start: el proceso principal termina de inmediato y el contenedor se para. Los servicios en contenedores corren en primer plano, siempre.
  • Usar docker kill por costumbre. Un SIGKILL sobre PostgreSQL fuerza una recuperación al arrancar y puede perder lo que hubiera en búfer. docker stop primero; kill solo si el otro no responde.
  • No capturar SIGTERM en la aplicación. El proceso muere de golpe con las conexiones abiertas. Con manejador, cierras el pool y sales con código 0.
  • Poner el temporizador de seguridad en 10 segundos o más. Debe ser menor que el periodo de gracia; si no, nunca llega a dispararse porque el SIGKILL llega antes.
  • Interpretar el 137 siempre como "lo maté yo". También lo produce el OOM killer cuando el contenedor se pasa de memoria. Se distinguen mirando .State.OOMKilled en docker inspect (lección 03-07).
  • Confundir 125 con 1. El 125 es de Docker: el contenedor ni siquiera existe, así que no hay logs que mirar. Revisa la línea de comandos.
  • Consejo: aumenta el --time en las bases de datos. docker stop --time 30 aurora-db da margen a PostgreSQL para hacer su checkpoint y cerrarse limpiamente.
  • Consejo: usa docker start -a cuando un contenedor arranque y muera enseguida: ves el arranque en directo sin tener que perseguirlo con docker logs.

Ejercicios

Ejercicio 1: recorre los siete estados

Usando alpine:3.20 y un contenedor llamado ciclo-completo que ejecute sleep 600, lleva el contenedor por esta secuencia y anota tras cada paso el resultado de docker inspect --format '{{.State.Status}} / código {{.State.ExitCode}}': crear sin arrancar → arrancar → pausar → reanudar → parar ordenadamente → volver a arrancar → matar → borrar. Responde: ¿cuál es el código de salida tras docker stop y por qué?, ¿y tras docker kill?, ¿en qué estado la memoria del proceso sigue ocupada?

Ejercicio 2: demuestra la pérdida de datos según el tipo de parada

Con un contenedor de redis:7-alpine llamado cache-prueba, escribe la clave catalogo:version con valor 1 y también un fichero /tmp/marca.txt con el mismo contenido. Después:

  1. Haz docker restart y comprueba qué sobrevive de las dos cosas.
  2. Haz docker stop seguido de docker start y comprueba lo mismo.
  3. Haz docker rm -f y vuelve a crear un contenedor con el mismo nombre e imagen. Comprueba lo mismo.

Explica los resultados en términos de memoria del proceso, capa de escritura y contenedor.

Ejercicio 3: mide el coste de no manejar señales

Prepara tres contenedores a partir de node:22-alpine que ejecuten estos tres programas y cronometra docker stop en cada uno, anotando además el código de salida:

  • A: node -e "setInterval(()=>{},1000)" — un proceso que no hace nada y no captura señales.
  • B: sh -c 'echo inicio && node -e "setInterval(()=>{},1000)"' — el mismo, pero envuelto en un shell con &&.
  • C: node -e "process.on('SIGTERM',()=>{console.log('adios');process.exit(0)});setInterval(()=>{},1000)" — con manejador de SIGTERM.

Explica los tres tiempos y los tres códigos, y di cuál de los tres se corresponde con auroralibros/aurora-api:1.1.0 y cuál con 1.2.0.

Soluciones

Solución al ejercicio 1

docker create --name ciclo-completo alpine:3.20 sleep 600
docker inspect --format '{{.State.Status}} / código {{.State.ExitCode}}' ciclo-completo
created / código 0
docker start ciclo-completo && docker inspect --format '{{.State.Status}} / código {{.State.ExitCode}}' ciclo-completo
docker pause ciclo-completo && docker inspect --format '{{.State.Status}} / código {{.State.ExitCode}}' ciclo-completo
docker unpause ciclo-completo && docker inspect --format '{{.State.Status}} / código {{.State.ExitCode}}' ciclo-completo
running / código 0
paused / código 0
running / código 0
time docker stop ciclo-completo
docker inspect --format '{{.State.Status}} / código {{.State.ExitCode}}' ciclo-completo
docker start ciclo-completo
docker kill ciclo-completo
docker inspect --format '{{.State.Status}} / código {{.State.ExitCode}}' ciclo-completo
docker rm ciclo-completo
ciclo-completo
real    0m0.156s
exited / código 143

ciclo-completo
ciclo-completo
exited / código 137

Las tres respuestas:

  • Tras docker stop: código 143, es decir, 128 + 15 = SIGTERM. sleep no instala ningún manejador de señales, así que se le aplica la acción por defecto de SIGTERM: terminar de inmediato. Por eso el real es de apenas 0,156 s y no hizo falta el SIGKILL. Es una parada limpia desde el punto de vista de Docker, aunque el proceso no ejecutara ninguna lógica de cierre propia: exactamente el caso de aurora-api:1.1.0.
  • Tras docker kill: código 137 (128 + 9 = SIGKILL), de forma instantánea y sin periodo de gracia. Compara los dos números: el 143 significa "se le pidió que terminara y terminó"; el 137 significa "se le terminó sin preguntar". Un 137 acompañado de una espera de diez segundos apuntaría además a un proceso que ignoró la petición.
  • La memoria sigue ocupada en paused. Los procesos están congelados por el freezer de cgroups, pero siguen existiendo con toda su memoria reservada. En exited no hay proceso y la memoria está liberada; lo que persiste es la capa de escritura en disco.

Solución al ejercicio 2

docker run -d --name cache-prueba redis:7-alpine
docker exec cache-prueba redis-cli SET catalogo:version 1
docker exec cache-prueba sh -c 'echo 1 > /tmp/marca.txt'

1. Tras docker restart:

docker restart cache-prueba && sleep 2
docker exec cache-prueba redis-cli GET catalogo:version
docker exec cache-prueba cat /tmp/marca.txt
(nil)
1

2. Tras docker stop + docker start:

docker stop cache-prueba && docker start cache-prueba && sleep 2
docker exec cache-prueba redis-cli GET catalogo:version
docker exec cache-prueba cat /tmp/marca.txt
(nil)
1

3. Tras docker rm -f y recrear:

docker rm -f cache-prueba
docker run -d --name cache-prueba redis:7-alpine && sleep 2
docker exec cache-prueba redis-cli GET catalogo:version
docker exec cache-prueba cat /tmp/marca.txt
docker rm -f cache-prueba
(nil)
cat: can't open '/tmp/marca.txt': No such file or directory

La explicación en tres niveles:

Nivel Qué sobrevive a restart/stop+start Qué sobrevive a rm
Memoria del proceso (clave de Redis) No. El proceso es nuevo, su memoria empieza vacía No
Capa de escritura (/tmp/marca.txt) Sí. Es disco, y el contenedor es el mismo No. Se destruye con el contenedor
Volumen (lección 03-06) , y ahí está la clave

Los casos 1 y 2 son equivalentes: restart es literalmente stop + start. Lo interesante es que ningún nivel de esta tabla sobrevive a un docker rm, y eso significa que hoy por hoy, si borras aurora-db, pierdes el catálogo de Aurora Libros. Esa es exactamente la demostración con la que abre la lección 03-06.

Solución al ejercicio 3

docker run -d --name senal-a node:22-alpine node -e "setInterval(()=>{},1000)"
docker run -d --name senal-b node:22-alpine sh -c 'echo inicio && node -e "setInterval(()=>{},1000)"'
docker run -d --name senal-c node:22-alpine node -e "process.on('SIGTERM',()=>{console.log('adios');process.exit(0)});setInterval(()=>{},1000)"

for c in senal-a senal-b senal-c; do
  echo "--- $c ---"
  { time docker stop "$c" ; } 2>&1 | grep real
  docker inspect --format 'código {{.State.ExitCode}}' "$c"
done
--- senal-a ---
real    0m0.142s
código 143
--- senal-b ---
real    0m10.238s
código 137
--- senal-c ---
real    0m0.118s
código 0
Caso Tiempo Código Por qué
A 0,14 s 143 node es PID 1 y recibe SIGTERM. Sin manejador propio, se aplica la acción por defecto: terminar. Rápido, pero abrupto: no cerró nada
B 10,24 s 137 El PID 1 es sh, que no reenvía la señal. Diez segundos de espera y SIGKILL. Lo peor de todos los mundos
C 0,12 s 0 node captura SIGTERM, ejecuta su lógica de cierre y sale por decisión propia con código 0

La correspondencia con Aurora Libros:

  • auroralibros/aurora-api:1.1.0 es el caso A. Forma exec (ENTRYPOINT ["node"]), así que la señal llega, pero sin manejador: paraba en 0,3 segundos con código 143 y dejaba el pool de PostgreSQL abierto. Eran los "0,3 segundos" que celebrabas en la lección 02-04, correctos pero incompletos.
  • auroralibros/aurora-api:1.2.0 es el caso C. Misma velocidad, pero cerrando el pool y el cliente de Redis y saliendo con código 0.
  • El caso B no debe existir nunca en tus imágenes. Es el que obtendrías con CMD node server.js && echo fin o con un entrypoint sin exec "$@".
docker rm senal-a senal-b senal-c

Conclusión

Ya sabes por qué unos contenedores viven y otros mueren al instante, y no hay ningún misterio: un contenedor dura exactamente lo que dure su proceso PID 1. ubuntu:24.04 ejecuta un bash sin entrada que termina limpiamente con código 0; nginx:alpine ejecuta nginx -g "daemon off;", que no termina nunca. Y aurora-api moría porque su propio código llamaba a process.exit(1). Conoces los siete estados y sus transiciones, y la diferencia crucial entre exited —dormido, con su capa de escritura, sus logs y su nombre intactos— y borrado de verdad con docker rm.

Dominas los verbos del ciclo de vida: start, stop, restart, pause/unpause con su freezer de cgroups que congela sin enviar ni una señal y sin liberar memoria, y kill como enviador de señales arbitrarias, incluido ese SIGHUP que recarga Nginx sin cortar una sola conexión. Y entiendes lo que pasa de verdad tras un docker stop: SIGTERM, diez segundos de gracia ajustables con --time, y SIGKILL. Lo has medido: 0,128 segundos con la forma exec frente a 10,271 segundos y código 137 con un shell de por medio que no reenvía señales.

Los códigos de salida han dejado de ser ruido. Sabes que por encima de 128 hay una señal escondida (137 = SIGKILL, 143 = SIGTERM, 139 = SIGSEGV), que 125, 126 y 127 apuntan a Docker o al arranque del comando y no a tu aplicación, y que un contenedor en estado Created tras un docker run significa que el ejecutable ni siquiera existía. Y has llevado la teoría al proyecto: auroralibros/aurora-api:1.2.0 captura SIGTERM y SIGINT, cierra el pool de PostgreSQL y el cliente de Redis, se protege con un temporizador de 8 segundos deliberadamente inferior a los 10 del periodo de gracia, y sale con código 0 en 0,187 segundos. Además, ya no se suicida porque Redis no esté: se queda vivo y lo registra, que es lo que debe hacer un servicio serio.

Con aurora-db y aurora-cache corriendo y aurora-api por fin capaz de mantenerse en pie, empiezas a tener algo parecido a una flota. Y una flota hay que saber mirarla y ordenarla. En la siguiente lección, Gestionando Contenedores, exprimirás docker ps hasta el fondo: cada columna explicada, el tamaño real de la capa de escritura con -s, los filtros por estado, nombre, imagen, etiqueta y salud, y las plantillas --format con las que montarás un pequeño panel de control de los cuatro servicios de Aurora Libros. Aprenderás a componer comandos con -q para operar sobre decenas de contenedores a la vez sin destrozar nada, a copiar ficheros entre el host y un contenedor con docker cp, a ver con docker diff qué ha cambiado un contenedor respecto a su imagen, y por qué docker commit jamás debe usarse para construir imágenes de verdad.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados