← maurobernal.com.ar

Etiqueta: docker-run

  • 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.

    EstadoSignificadoCómo llegar
    createdCreado pero nunca iniciadodocker create
    runningEn ejecucióndocker start / docker run
    pausedProcesos suspendidos (SIGSTOP)docker pause
    restartingReiniciándose (restart policy activa)Fallo del proceso principal
    exitedDetenido — con código de salidadocker stop o fallo
    deadFallo en la eliminaciónError 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) →

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)