Solución de Problemas: Contenedores Docker que Salen Inmediatamente

DockerBeginner
Practicar Ahora

Introducción

En este laboratorio práctico, aprenderá a identificar y resolver un problema común de Docker: contenedores que se cierran inmediatamente después de iniciarse. Este problema a menudo confunde a los principiantes y puede ocurrir por varias razones, que van desde errores de configuración hasta problemas de la aplicación.

Al completar este laboratorio, comprenderá el ciclo de vida de los contenedores Docker, aprenderá a diagnosticar las salidas inmediatas de los contenedores, dominará las técnicas de depuración e implementará las mejores prácticas para garantizar que sus contenedores se ejecuten de manera confiable. Estas habilidades son esenciales para cualquier persona que trabaje con Docker en entornos de desarrollo o producción.

Comprensión de los Conceptos Básicos de los Contenedores Docker

Comencemos explorando los fundamentos de los contenedores Docker y familiarizándonos con los comandos básicos utilizados para administrar contenedores.

¿Qué es un Contenedor Docker?

Un contenedor Docker es un paquete de software ligero, autónomo y ejecutable que incluye todo lo necesario para ejecutar una aplicación:

  • Código
  • Tiempo de ejecución (Runtime)
  • Herramientas del sistema
  • Bibliotecas
  • Configuración

Los contenedores se ejecutan de forma aislada del sistema host y de otros contenedores, proporcionando consistencia en diferentes entornos.

Explorando Docker en su Sistema

Primero, verifiquemos que Docker esté correctamente instalado y en ejecución en su sistema:

docker --version

Debería ver una salida similar a:

Docker version 20.10.21, build baeda1f

A continuación, comprobemos si hay contenedores en ejecución actualmente:

docker ps

Este comando enumera los contenedores en ejecución. Como aún no hemos iniciado ninguno, solo debería ver los encabezados de las columnas:

CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Para ver todos los contenedores, incluidos los detenidos:

docker ps -a

Comprensión del Ciclo de Vida del Contenedor

El ciclo de vida del contenedor Docker consta de varios estados:

  1. Created (Creado): Se crea un contenedor pero aún no se ha iniciado
  2. Running (En ejecución): El contenedor está ejecutando sus procesos definidos
  3. Paused (Pausado): Los procesos del contenedor se suspenden temporalmente
  4. Stopped (Detenido): El contenedor ha salido o se ha detenido
  5. Deleted (Eliminado): El contenedor ha sido eliminado del sistema

Ejecutemos un contenedor simple y observemos su ciclo de vida:

docker run hello-world

Debería ver una salida similar a:

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.
...

Observe que este contenedor se ejecutó y salió inmediatamente después de mostrar el mensaje. Este es en realidad un comportamiento normal para el contenedor hello-world, ya que está diseñado para simplemente mostrar un mensaje y salir.

Compruebe el contenedor en la lista de todos los contenedores:

docker ps -a

Debería ver su contenedor hello-world en la lista con un estado "Exited" (Salido):

CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS                      PORTS     NAMES
a1b2c3d4e5f6   hello-world   "/hello"   30 seconds ago   Exited (0) 30 seconds ago             peaceful_hopper

El código de salida (0) indica que el contenedor salió con éxito sin errores.

Comandos Clave de Docker para la Gestión de Contenedores

Aquí hay algunos comandos esenciales de Docker que utilizará a lo largo de este laboratorio:

  • docker run [OPTIONS] IMAGE [COMMAND]: Crear e iniciar un contenedor
  • docker ps: Listar los contenedores en ejecución
  • docker ps -a: Listar todos los contenedores (incluidos los detenidos)
  • docker logs [CONTAINER_ID]: Ver los registros del contenedor
  • docker inspect [CONTAINER_ID]: Obtener información detallada del contenedor
  • docker exec -it [CONTAINER_ID] [COMMAND]: Ejecutar un comando en un contenedor en ejecución
  • docker stop [CONTAINER_ID]: Detener un contenedor en ejecución
  • docker rm [CONTAINER_ID]: Eliminar un contenedor

Ahora que comprende los conceptos básicos de los contenedores Docker y su ciclo de vida, pasaremos a la solución de problemas de contenedores que salen inesperadamente.

Identificación de Problemas de Salida de Contenedores

En este paso, crearemos un contenedor Docker que sale inmediatamente y aprenderemos a diagnosticar el problema.

Ejecución de un Contenedor que Sale Inmediatamente

Primero, intentemos ejecutar un contenedor Ubuntu:

docker run ubuntu

Notará algo interesante: el comando se completa instantáneamente y regresa a su indicador. ¿Dónde está nuestro contenedor Ubuntu? Comprobémoslo:

docker ps

No hay contenedores en ejecución. Ahora compruebe todos los contenedores, incluidos los detenidos:

docker ps -a

Verá una salida similar a:

CONTAINER ID   IMAGE         COMMAND       CREATED          STATUS                      PORTS     NAMES
f7d9e7f6543d   ubuntu        "/bin/bash"   10 seconds ago   Exited (0) 10 seconds ago             focused_galileo
a1b2c3d4e5f6   hello-world   "/hello"      10 minutes ago   Exited (0) 10 minutes ago             peaceful_hopper

El contenedor Ubuntu se inició y luego salió inmediatamente con un código de estado de 0, lo que indica que salió sin errores. Este es el comportamiento esperado, pero puede ser confuso para los principiantes.

Comprensión de Por Qué Salen los Contenedores

Los contenedores están diseñados para ejecutar un comando o proceso específico. Cuando ese proceso se completa o sale, el contenedor se detiene. Este es un principio fundamental del diseño de Docker.

El contenedor Ubuntu salió inmediatamente porque:

  1. El comando predeterminado para la imagen Ubuntu es /bin/bash
  2. Cuando se ejecuta sin las banderas -it (interactivo, terminal), no hay entrada para el shell bash
  3. Sin entrada y sin un comando específico para ejecutar, bash sale inmediatamente
  4. Cuando el proceso principal (bash) sale, el contenedor se detiene

Visualización de Registros e Información del Contenedor

Examinemos los registros de nuestro contenedor Ubuntu que salió para comprender qué sucedió. Primero, encuentre el ID del contenedor en su salida docker ps -a, luego:

docker logs CONTAINER_ID

Reemplace CONTAINER_ID con el ID de su contenedor real. Es probable que no vea ninguna salida porque el contenedor no produjo ningún registro antes de salir.

Para obtener información más detallada sobre el contenedor:

docker inspect CONTAINER_ID

Esto mostrará un gran objeto JSON con la configuración del contenedor y la información de estado.

Centrémonos en el código de salida:

docker inspect CONTAINER_ID --format='{{.State.ExitCode}}'

Debería ver:

0

Esto confirma que el contenedor salió normalmente, no debido a un error.

Mantener un Contenedor en Ejecución

Para mantener el contenedor Ubuntu en ejecución, necesitamos:

  1. Proporcionar una sesión interactiva, o
  2. Anular el comando predeterminado con un proceso de larga duración

Intentemos el enfoque interactivo:

docker run -it ubuntu

Ahora está dentro del contenedor con un indicador bash:

root@3a4b5c6d7e8f:/#

El contenedor permanece en ejecución mientras esta sesión bash esté activa. Escriba exit o presione Ctrl+D para salir del contenedor.

exit

Alternativamente, podemos mantener un contenedor en ejecución proporcionando un comando que no se complete inmediatamente:

docker run -d ubuntu sleep 300

Esto ejecuta el contenedor Ubuntu y ejecuta el comando sleep 300, que mantendrá el contenedor en ejecución durante 300 segundos (5 minutos).

Compruebe que el contenedor se está ejecutando:

docker ps

Debería ver su contenedor en el estado de ejecución:

CONTAINER ID   IMAGE     COMMAND       CREATED          STATUS          PORTS     NAMES
9a8b7c6d5e4f   ubuntu    "sleep 300"   10 seconds ago   Up 10 seconds             hopeful_hopper

Al diagnosticar contenedores que salen inmediatamente, recuerde estos puntos clave:

  1. Los contenedores salen cuando su proceso principal se completa
  2. Compruebe los registros y los códigos de salida para comprender por qué se detuvieron
  3. Si un contenedor debe seguir ejecutándose, asegúrese de que su proceso principal no salga

Solución de Problemas Comunes de Salida de Contenedores

Ahora que entendemos por qué los contenedores salen inmediatamente, exploremos las causas comunes de las salidas inesperadas de contenedores y cómo solucionar los problemas.

Creación de un Contenedor con un Problema

Creemos una situación simple donde un contenedor sale inesperadamente. Primero, cree un directorio para nuestros archivos de prueba:

mkdir -p ~/project/docker-exit-test
cd ~/project/docker-exit-test

Ahora, cree un script Python simple con un error:

nano app.py

Agregue el siguiente código:

import os

## Attempt to read a required environment variable
database_url = os.environ['DATABASE_URL']

print(f"Connecting to database: {database_url}")
print("Application running...")

## Rest of the application code would go here

Guarde y salga del archivo (presione Ctrl+O, Enter, luego Ctrl+X).

Ahora, cree un Dockerfile para construir una imagen con esta aplicación:

nano Dockerfile

Agregue el siguiente contenido:

FROM python:3.9-slim

WORKDIR /app

COPY app.py .

CMD ["python", "app.py"]

Guarde y salga del archivo.

Construya la imagen Docker:

docker build -t exit-test-app .

Debería ver una salida que indica que la imagen se construyó correctamente:

Successfully built a1b2c3d4e5f6
Successfully tagged exit-test-app:latest

Ahora, ejecute el contenedor:

docker run exit-test-app

Debería ver que el contenedor sale inmediatamente con un error:

Traceback (most recent call last):
  File "/app/app.py", line 4, in <module>
    database_url = os.environ['DATABASE_URL']
  File "/usr/local/lib/python3.9/os.py", line 679, in __getitem__
    raise KeyError(key) from None
KeyError: 'DATABASE_URL'

El contenedor salió porque nuestro script Python esperaba una variable de entorno que no se proporcionó.

Diagnóstico del Problema

Cuando un contenedor sale inesperadamente, siga estos pasos para la solución de problemas:

  1. Compruebe el código de salida para determinar si es un error:
docker ps -a

Busque su contenedor y anote el código de salida. Debería ser distinto de cero, lo que indica un error:

CONTAINER ID   IMAGE           COMMAND            CREATED          STATUS                     PORTS     NAMES
b1c2d3e4f5g6   exit-test-app   "python app.py"   20 seconds ago   Exited (1) 19 seconds ago           vigilant_galileo
  1. Examine los registros del contenedor:
docker logs $(docker ps -a -q --filter ancestor=exit-test-app --latest)

Esto recupera los registros del contenedor más reciente creado a partir de nuestra imagen.

El mensaje de error muestra claramente el problema: el script Python está intentando acceder a una variable de entorno llamada DATABASE_URL que no existe.

Solución del Problema

Ahora, solucionemos el problema proporcionando la variable de entorno que falta:

docker run -e DATABASE_URL=postgresql://user:password@db:5432/mydatabase exit-test-app

Ahora debería ver que el contenedor se ejecuta correctamente:

Connecting to database: postgresql://user:password@db:5432/mydatabase
Application running...

El contenedor aún sale, pero esta vez es porque nuestro script llega al final y termina normalmente. Si quisiéramos que el contenedor siguiera ejecutándose, necesitaríamos modificar nuestro script para incluir un bucle infinito o un proceso de larga duración.

Causas Comunes de Salidas de Contenedores

Aquí hay varias razones comunes por las que los contenedores podrían salir inesperadamente:

  1. Variables de entorno faltantes: Como acabamos de demostrar
  2. Dependencias ausentes: Bibliotecas o paquetes del sistema faltantes
  3. Fallos de conexión: No se puede acceder a una base de datos u otro servicio
  4. Problemas de permisos: Permisos insuficientes para acceder a archivos o recursos
  5. Limitaciones de recursos: El contenedor se queda sin memoria o CPU
  6. Fallos de la aplicación: Errores en el código de la aplicación

Técnicas de Solución de Problemas

Para cada uno de estos problemas, aquí hay enfoques efectivos para la solución de problemas:

  1. Revisar los registros: Siempre compruebe primero los registros del contenedor con docker logs
  2. Anular el punto de entrada (entrypoint): Use docker run --entrypoint /bin/sh -it my-image para ingresar al contenedor e investigar
  3. Agregar declaraciones de depuración: Modifique su aplicación para agregar más registros
  4. Comprobar el uso de recursos: Use docker stats para monitorear el uso de recursos del contenedor
  5. Inspeccionar el entorno: Ejecute docker exec -it CONTAINER_ID env para verificar las variables de entorno

Intentemos la técnica de anulación del punto de entrada con nuestra imagen:

docker run --entrypoint /bin/sh -it exit-test-app

Ahora está dentro del contenedor con un shell. Puede explorar el entorno:

ls -la
cat app.py
echo $DATABASE_URL

Verá que DATABASE_URL no está configurado. Salga del contenedor cuando haya terminado:

exit

Al comprender por qué los contenedores salen y aplicar estas técnicas de solución de problemas, puede diagnosticar y resolver rápidamente la mayoría de los problemas de salida de contenedores.

Implementación de las Mejores Prácticas para la Fiabilidad de los Contenedores

Ahora que entendemos cómo solucionar los problemas de salida de los contenedores, implementemos las mejores prácticas para que nuestros contenedores sean más fiables y robustos. Mejoraremos nuestra aplicación de ejemplo con un manejo adecuado de errores, comprobaciones de estado y registro.

1. Mejora del Manejo de Errores

Modifiquemos nuestra aplicación Python para manejar las variables de entorno faltantes con elegancia:

cd ~/project/docker-exit-test
nano app.py

Actualice el contenido a:

import os
import time
import sys

## Get environment variables with defaults
database_url = os.environ.get('DATABASE_URL', 'sqlite:///default.db')

print(f"Connecting to database: {database_url}")

## Simulate a long-running process
try:
    print("Application running... Press Ctrl+C to exit")
    counter = 0
    while True:
        counter += 1
        print(f"Application heartbeat: {counter}")
        time.sleep(10)
except KeyboardInterrupt:
    print("Application shutting down gracefully...")
    sys.exit(0)

Guarde y salga del archivo.

Esta versión mejorada:

  • Usa os.environ.get() con un valor predeterminado en lugar de generar una excepción
  • Implementa un bucle de larga duración para mantener el contenedor activo
  • Maneja el apagado correcto cuando se termina

Reconstruyamos la imagen:

docker build -t exit-test-app:v2 .

Y ejecute el contenedor mejorado:

docker run -d --name improved-app exit-test-app:v2

Compruebe que el contenedor se está ejecutando:

docker ps

Debería ver su contenedor en ejecución:

CONTAINER ID   IMAGE              COMMAND            CREATED          STATUS          PORTS     NAMES
c1d2e3f4g5h6   exit-test-app:v2   "python app.py"   10 seconds ago   Up 10 seconds             improved-app

Vea los registros para confirmar que está funcionando:

docker logs improved-app

Debería ver una salida como:

Connecting to database: sqlite:///default.db
Application running... Press Ctrl+C to exit
Application heartbeat: 1
Application heartbeat: 2

2. Implementación de una Comprobación de Estado en Dockerfile

Las comprobaciones de estado permiten a Docker monitorear el estado de su contenedor. Actualicemos nuestro Dockerfile para incluir una comprobación de estado:

nano Dockerfile

Actualícelo a:

FROM python:3.9-slim

WORKDIR /app

## Install curl for health checks
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

COPY app.py .

## Add a health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

## Expose port for the health check endpoint
EXPOSE 8080

CMD ["python", "app.py"]

Ahora necesitamos agregar un punto final de comprobación de estado a nuestra aplicación:

nano app.py

Reemplace el contenido con:

import os
import time
import sys
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler

## Get environment variables with defaults
database_url = os.environ.get('DATABASE_URL', 'sqlite:///default.db')

## Simple HTTP server for health checks
class HealthRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/health':
            self.send_response(200)
            self.send_header('Content-type', 'text/plain')
            self.end_headers()
            self.wfile.write(b'OK')
        else:
            self.send_response(404)
            self.end_headers()

def run_health_server():
    server = HTTPServer(('0.0.0.0', 8080), HealthRequestHandler)
    print("Starting health check server on port 8080")
    server.serve_forever()

## Start health check server in a separate thread
health_thread = threading.Thread(target=run_health_server, daemon=True)
health_thread.start()

print(f"Connecting to database: {database_url}")

## Main application loop
try:
    print("Application running... Press Ctrl+C to exit")
    counter = 0
    while True:
        counter += 1
        print(f"Application heartbeat: {counter}")
        time.sleep(10)
except KeyboardInterrupt:
    print("Application shutting down gracefully...")
    sys.exit(0)

Guarde y salga del archivo.

Reconstruya la imagen con nuestra comprobación de estado:

docker build -t exit-test-app:v3 .

Ejecute el contenedor con la nueva versión:

docker run -d --name healthcheck-app -p 8080:8080 exit-test-app:v3

Después de unos 30 segundos, compruebe el estado de salud:

docker inspect --format='{{.State.Health.Status}}' healthcheck-app

Debería ver:

healthy

También puede probar el punto final de estado directamente:

curl http://localhost:8080/health

Esto debería devolver OK.

3. Uso de Políticas de Reinicio de Docker

Docker proporciona políticas de reinicio para reiniciar automáticamente los contenedores cuando salen o encuentran errores:

docker run -d --restart=on-failure:5 --name restart-app exit-test-app:v3

Esta política reiniciará el contenedor hasta 5 veces si sale con un código distinto de cero.

Políticas de reinicio disponibles:

  • no: Nunca reiniciar (predeterminado)
  • always: Siempre reiniciar independientemente del estado de salida
  • unless-stopped: Siempre reiniciar a menos que se detenga manualmente
  • on-failure[:max-retries]: Reiniciar solo en la salida distinta de cero

4. Establecimiento de Límites de Recursos

Para evitar que los contenedores se bloqueen debido al agotamiento de los recursos, establezca los límites de recursos apropiados:

docker run -d --name resource-limited-app \
  --memory=256m \
  --cpus=0.5 \
  exit-test-app:v3

Esto limita el contenedor a 256 MB de memoria y la mitad de un núcleo de CPU.

5. Configuración de Registro Adecuada

Para una mejor depuración, configure su aplicación para que genere registros estructurados:

docker run -d --name logging-app \
  --log-driver=json-file \
  --log-opt max-size=10m \
  --log-opt max-file=3 \
  exit-test-app:v3

Esto configura el contenedor para usar el controlador de registro JSON con rotación de registros (máximo 3 archivos de 10 MB cada uno).

Resumen de las Mejores Prácticas

Al implementar estas mejores prácticas, ha mejorado significativamente la fiabilidad de sus contenedores Docker:

  1. Manejo de errores correcto con valores predeterminados
  2. Comprobaciones de estado del contenedor para el monitoreo
  3. Políticas de reinicio apropiadas para la recuperación automática
  4. Límites de recursos para evitar el agotamiento de recursos
  5. Configuración de registro adecuada para facilitar la solución de problemas

Estas técnicas le ayudarán a crear contenedores resilientes que puedan recuperarse de fallos y proporcionar una mejor observabilidad cuando ocurren problemas.

Resumen

En este laboratorio, ha aprendido habilidades esenciales para solucionar problemas y resolver problemas de salida de contenedores Docker:

  • Comprender el ciclo de vida del contenedor Docker y por qué los contenedores salen cuando su proceso principal se completa
  • Identificar las causas comunes de las salidas inmediatas de contenedores a través de registros y códigos de salida
  • Implementar un manejo de errores robusto en aplicaciones en contenedores
  • Agregar comprobaciones de estado del contenedor para monitorear el estado de la aplicación
  • Configurar políticas de reinicio para la recuperación automática de fallos
  • Establecer límites de recursos apropiados para evitar bloqueos de contenedores
  • Implementar estrategias de registro adecuadas para facilitar la depuración

Estas habilidades forman la base de las implementaciones fiables de contenedores Docker. A medida que continúe trabajando con Docker en escenarios del mundo real, encontrará que estas técnicas de solución de problemas son invaluables para mantener aplicaciones en contenedores estables y resilientes.

Recuerde que las causas más comunes de las salidas inmediatas de contenedores son:

  1. El proceso principal que completa su tarea (por diseño)
  2. Variables de entorno o configuración faltantes
  3. Errores o excepciones de la aplicación
  4. Restricciones de recursos o problemas de conectividad

Al aplicar las técnicas de diagnóstico y las mejores prácticas cubiertas en este laboratorio, puede identificar y resolver rápidamente estos problemas, asegurando que sus contenedores Docker se ejecuten de manera fiable en cualquier entorno.