docker run y todo lo que nadie te explica del ciclo de vida de un contenedor
Son las 3 de la mañana. Me llega una alerta: un contenedor en producción está en estado Exited (137). No arranca. No hay logs visibles. Y yo no entiendo qué pasó. Esa noche aprendí más sobre el ciclo de vida de un contenedor que en semanas de tutoriales.
Los estados de un contenedor Docker
Un contenedor no está simplemente «prendido o apagado». Tiene un ciclo de vida con varios estados bien definidos, y entenderlos es fundamental para diagnosticar problemas.
| Estado | Significado | Cómo llegar |
|---|---|---|
created | Creado pero nunca iniciado | docker create |
running | En ejecución | docker start / docker run |
paused | Procesos suspendidos (SIGSTOP) | docker pause |
restarting | Reiniciándose (restart policy activa) | Fallo del proceso principal |
exited | Detenido — con código de salida | docker stop o fallo |
dead | Fallo en la eliminación | Error interno del daemon |
Ese Exited (137) de las 3am significaba: el proceso fue terminado por una señal 9 (SIGKILL). En mi caso, el OOMKiller del kernel mató el contenedor porque superó el límite de memoria. El código de salida 137 = 128 + 9. Una vez que entendés la convención, el diagnóstico es inmediato.
docker run: el comando que más usás y menos conocés
El 80% de las veces empezamos con docker run nombre-imagen y nos conformamos con eso. Pero docker run tiene decenas de flags que cambian completamente el comportamiento del contenedor. Acá están los que realmente uso:
Flags de ejecución básicos
# -d: modo detached (background) - casi siempre lo quiero en producción
docker run -d nginx
# --name: nombre legible - SIEMPRE lo pongo
docker run -d --name mi-nginx nginx
# -p: mapeo de puertos host:contenedor
docker run -d --name mi-nginx -p 8080:80 nginx
# -e: variables de entorno
docker run -d --name mi-api -e ASPNETCORE_ENVIRONMENT=Production -e ConnectionStrings__Default="Server=db;Database=miapp" mi-api:latest
# --env-file: variables desde archivo (no las expongo en bash history)
docker run -d --name mi-api --env-file .env.prod mi-api:latest
Políticas de reinicio: la diferencia entre prod y dev
# no (default): no reinicia nunca
docker run --restart=no mi-app
# always: siempre reinicia, incluso al reboot del host
# (úsalo para servicios críticos en producción)
docker run -d --restart=always --name mi-api mi-api:latest
# unless-stopped: como always, pero respeta docker stop manual
docker run -d --restart=unless-stopped --name mi-api mi-api:latest
# on-failure:N: reinicia solo si falla, máximo N veces
docker run -d --restart=on-failure:3 mi-worker:latest
Límites de recursos: lo que me hubiera evitado la alerta de las 3am
# Limitar memoria y CPU
docker run -d --name mi-api --memory="512m" \ # máximo 512MB de RAM
--memory-swap="1g" \ # swap incluido
--cpus="0.5" \ # máximo 50% de un CPU
--restart=unless-stopped mi-api:latest
# Ver uso de recursos en tiempo real
docker stats
docker stats mi-api # solo ese contenedor
Los comandos de gestión que uso todos los días
# Listar contenedores
docker ps # en ejecución
docker ps -a # todos, incluso detenidos
docker ps -a --format "table {{.Names}} {{.Status}} {{.Ports}}"
# Logs - mi herramienta principal de diagnóstico
docker logs mi-api
docker logs -f mi-api # follow (como tail -f)
docker logs --tail 100 mi-api
docker logs --since 1h mi-api # última hora
docker logs --since "2026-03-11T03:00:00" mi-api
# Ejecutar comandos dentro del contenedor
docker exec mi-api ls /app
docker exec -it mi-api bash # shell interactivo
docker exec -it mi-api sh # para contenedores Alpine (sin bash)
# Copiar archivos entre host y contenedor
docker cp mi-api:/app/logs/error.log ./error.log
docker cp ./config.json mi-api:/app/config.json
Diagnosticar el problema de las 3am: mi checklist
Cuando me llega una alerta de contenedor caído, sigo siempre el mismo proceso:
# 1. Ver el estado y el código de salida
docker ps -a | grep mi-api
# CONTAINER ID IMAGE STATUS NAMES
# a1b2c3d4e5f6 mi-api Exited (137) 2 minutes ago mi-api
# 2. Ver los últimos logs ANTES de que muriera
docker logs --tail 50 mi-api
# 3. Inspeccionar el contenedor completo
docker inspect mi-api | python3 -m json.tool | grep -A5 "State"
# 4. Ver eventos del daemon
docker system events --since 1h --filter container=mi-api
# Códigos de salida comunes:
# 0 → salida normal (proceso terminó correctamente)
# 1 → error genérico de la aplicación
# 137 → SIGKILL (OOM Killer o docker kill)
# 143 → SIGTERM (docker stop - salida limpia)
# 126 → permisos insuficientes para ejecutar el comando
# 127 → comando no encontrado
Detener y eliminar contenedores correctamente
# Detener con gracia (envía SIGTERM, espera 10s, luego SIGKILL)
docker stop mi-api
# Detener más rápido (menos tiempo de espera)
docker stop --time=5 mi-api
# Forzar eliminación inmediata (SIGKILL directo - para emergencias)
docker kill mi-api
# Eliminar contenedor detenido
docker rm mi-api
# Detener Y eliminar en uno
docker rm -f mi-api
# Limpiar todos los contenedores detenidos
docker container prune
Lo que aprendí esa noche
El contenedor que falló a las 3am no tenía límites de memoria configurados, y el proceso .NET tenía una fuga de memoria. El OOMKiller del kernel lo mató antes de que pudiera afectar al nodo completo. En cierta forma, funcionó como debía. Desde entonces, todos mis contenedores de producción tienen --memory configurado y política --restart=unless-stopped. Una mala noche bien aprovechada.
← Artículo anterior: Cómo escribir Dockerfiles | Serie Docker Completo | Próximo: Volúmenes y persistencia →
← Artículo anterior: Mi guía para escribir Dockerfiles que no me den vergüenza | Serie Docker Completo | Próximo: Cuando perdí datos de producción por no usar volúmenes (y cómo no repetirlo) →
