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
- Los estados de un contenedor
- El PID 1 y la regla fundamental
- Por qué
ubuntumuere ynginxno start,stopyrestartpauseyunpause: congelar sin matarkilly el envío de señales concretas- Parada ordenada frente a parada forzada
- La forma shell del
CMDy sus diez segundos, medidos - Códigos de salida y su tabla de diagnóstico
- Apagado ordenado de
aurora-api docker waitydocker rename
- 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 |
Sí | Sí | 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
exitedsigue 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 condocker start. Es un contenedor dormido, no un contenedor borrado. docker rmes 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-dbEse 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.
- 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:
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:
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.
- Por qué
ubuntu muere y nginx no
ubuntu muere y nginx noEsta 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.04tieneCMD ["/bin/bash"]. Unbashsin 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:alpineejecutanginx -g "daemon off;". Esedaemon offes 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}}"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}}"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 onen 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ó.
start, stop y restart
start, stop y restart| Comando | Qué hace | ¿Mismo contenedor? | ¿Mismo PID? |
|---|---|---|---|
docker stop |
SIGTERM, espera, SIGKILL | — | — |
docker start |
Vuelve a lanzar el comando original | Sí, mismo ID y misma capa de escritura | No, PID nuevo |
docker restart |
stop + start en un solo paso |
Sí | 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:favoritoDos resultados opuestos en el mismo comando:
/tmp/nota.txtsigue ahí: está escrito en la capa de escritura del contenedor, que sobrevive a paradas y arranques. Solo desaparece condocker 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:
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:
-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.
pause y unpause: congelar sin matar
pause y unpause: congelar sin matardocker 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:
Ese comando se queda colgado indefinidamente: el contenedor no puede responder porque no se está ejecutando. Púlsalo con Ctrl+C y descongélalo:
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.
kill y el envío de señales concretas
kill y el envío de señales concretasdocker kill aurora-cache
docker ps -a --filter name=aurora-cache --format "{{.Names}}: {{.Status}}"
docker start aurora-cachedocker 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:
| 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}}"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.
- 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:
- Docker envía el STOPSIGNAL de la imagen (por defecto
SIGTERM) al PID 1. Recuerda de la lección 02-04 que Nginx declaraSTOPSIGNAL SIGQUIT. - Espera el periodo de gracia: 10 segundos por defecto.
- 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? | Sí | 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.
- La forma shell del
CMD y sus diez segundos, medidos
CMD y sus diez segundos, medidosAquí 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.jsAhora 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 -3Ahí 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:
Ochenta veces más lento. Y los códigos de salida cuentan el resto de la historia:
Reconstruyamos lo ocurrido en el segundo caso:
docker stopenvía SIGQUIT (el STOPSIGNAL de Nginx) al PID 1, que essh.shno tiene ningún manejador para esa señal ni sabe nada de reenviarla a sus hijos. La ignora o termina él solo, peronginxno se entera de nada.- Docker espera sus diez segundos completos.
- 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 undocker-entrypoint.shque termine enexec "$@", como hiciste en la lección 02-04.
Limpia la demostración:
- 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-apiLa 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 /etcdocker: 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 denieddocker 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
125Observa 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.
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.
- Apagado ordenado de
aurora-api
aurora-apiHa 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/apiY 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}}"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-paradaapi-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: 0Las 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
nodeporque 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.isOpenera 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 wait y docker rename
docker wait y docker renameDos 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-lentaSe 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 migraciondocker 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-cacheEs 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
Exitedestá borrado. Sigue existiendo, ocupa su nombre y conserva capa de escritura y logs. Se borra condocker rm, no condocker stop. - Poner un servicio en modo demonio dentro del contenedor.
nginxsindaemon off,httpd -k start,postgresconpg_ctl start: el proceso principal termina de inmediato y el contenedor se para. Los servicios en contenedores corren en primer plano, siempre. - Usar
docker killpor costumbre. Un SIGKILL sobre PostgreSQL fuerza una recuperación al arrancar y puede perder lo que hubiera en búfer.docker stopprimero;killsolo 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.OOMKilledendocker 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
--timeen las bases de datos.docker stop --time 30 aurora-dbda margen a PostgreSQL para hacer su checkpoint y cerrarse limpiamente. - Consejo: usa
docker start -acuando un contenedor arranque y muera enseguida: ves el arranque en directo sin tener que perseguirlo condocker 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:
- Haz
docker restarty comprueba qué sobrevive de las dos cosas. - Haz
docker stopseguido dedocker starty comprueba lo mismo. - Haz
docker rm -fy 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-completodocker 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-completotime 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-completoLas tres respuestas:
- Tras
docker stop: código 143, es decir, 128 + 15 = SIGTERM.sleepno instala ningún manejador de señales, así que se le aplica la acción por defecto de SIGTERM: terminar de inmediato. Por eso elreales 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 deaurora-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. Enexitedno 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.txt2. 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.txt3. 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-pruebaLa 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) | Sí | Sí, 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.0es 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.0es 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 fino con un entrypoint sinexec "$@".
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
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
