Compartir comentarios
Las respuestas se generan en base a la documentación.

Inicio rápido de Docker Compose

Este tutorial tiene como objetivo introducir los conceptos fundamentales de Docker Compose guiándote en el desarrollo de una aplicación web básica en Python.

Utilizando el framework Flask, la aplicación cuenta con un contador de visitas en Redis, lo que proporciona un ejemplo práctico de cómo se puede aplicar Docker Compose en escenarios de desarrollo web. Los conceptos que se muestran aquí deberían ser comprensibles incluso si no estás familiarizado con Python.

Requisitos previos

Asegúrate de tener:

Paso 1: Configurar el proyecto

  1. Crea un directorio para el proyecto:

    $ mkdir compose-demo
    $ cd compose-demo
    
  2. Crea el archivo app.py en el directorio de tu proyecto y añade lo siguiente:

    import os
    import redis
    from flask import Flask
    
    app = Flask(__name__)
    cache = redis.Redis(
        host=os.getenv("REDIS_HOST", "redis"),
        port=int(os.getenv("REDIS_PORT", "6379")),
    )
    
    @app.route("/")
    def hello():
        count = cache.incr("hits")
        return f"Hello from Docker! I have been seen {count} time(s).\n"

    La aplicación lee los detalles de su conexión a Redis desde variables de entorno, con valores predeterminados razonables para que funcione de inmediato.

  3. Crea el archivo requirements.txt en el directorio de tu proyecto y añade lo siguiente:

    flask
    redis
  4. Crea un Dockerfile:

    # syntax=docker/dockerfile:1
    FROM python:3.12-alpine  # Construye una imagen a partir de la imagen de Python 3.12
    WORKDIR /code  # Establece el directorio de trabajo en `/code`
    ENV FLASK_APP=app.py  # Establece las variables de entorno utilizadas por el comando `flask`
    ENV FLASK_RUN_HOST=0.0.0.0
    RUN apk add --no-cache gcc musl-dev linux-headers  # Instala `gcc` y otras dependencias
    COPY requirements.txt .  # Copia `requirements.txt`
    RUN pip install -r requirements.txt  # Instala las dependencias de Python
    COPY . .  # Copia el directorio actual `.` del proyecto en el directorio de trabajo `.` de la imagen
    EXPOSE 5000
    CMD ["flask", "run", "--debug"]  # Establece el comando predeterminado para el contenedor a `flask run --debug`
    Important

    Asegúrate de que el archivo se llame Dockerfile sin extensión. Algunos editores añaden .txt automáticamente, lo que hace que la construcción falle.

    Para obtener más información sobre cómo escribir Dockerfiles, consulta la referencia de Dockerfile.

  5. Crea un archivo .env para almacenar los valores de configuración:

    APP_PORT=8000
    REDIS_HOST=redis
    REDIS_PORT=6379

    Compose lee automáticamente el archivo .env y hace que estos valores estén disponibles para su interpolación en tu archivo compose.yaml. Para este ejemplo, los beneficios son modestos, pero en la práctica, mantener la configuración fuera del archivo de Compose facilita:

    • Cambiar valores entre entornos sin tener que editar el archivo YAML
    • Evitar subir secretos al sistema de control de versiones
    • Reutilizar valores en múltiples servicios
  6. Crea un archivo .dockerignore para mantener los archivos innecesarios fuera del contexto de construcción:

    .env
    *.pyc
    __pycache__
    redis-data

    Docker envía todo lo que se encuentra en el directorio de tu proyecto al demonio cuando construye una imagen. Sin .dockerignore, eso incluye tu archivo .env (que puede contener secretos) y cualquier código de bytes de Python en caché. Excluirlos mantiene las construcciones rápidas y evita integrar accidentalmente valores sensibles en una capa de la imagen.

Paso 2: Definir e iniciar tus servicios

Compose simplifica el control de todo tu stack de aplicaciones, facilitando la gestión de servicios, redes y volúmenes en un solo archivo de configuración YAML.

  1. Crea el archivo compose.yaml en el directorio de tu proyecto y pega lo siguiente:

    services:
      web:
        build: .
        ports:
          - "${APP_PORT}:5000"
        environment:
          - REDIS_HOST=${REDIS_HOST}
          - REDIS_PORT=${REDIS_PORT}
    
      redis:
        image: redis:alpine

    Este archivo de Compose define dos servicios:

    • El servicio web utiliza una imagen que se construye a partir del Dockerfile en el directorio actual. Mapea el puerto 8000 del host al puerto 5000 del contenedor donde Flask escucha de forma predeterminada.

    • El servicio redis utiliza una imagen pública de Redis descargada del registro Docker Hub.

    Para obtener más información sobre el archivo compose.yaml, consulta Cómo funciona Compose.

  2. Inicia tu aplicación:

    $ docker compose up
    

    Con un solo comando, creas e inicias todos los servicios a partir de tu archivo de configuración. Compose construye tu imagen web, descarga la imagen de Redis e inicia ambos contenedores.

  3. Abre http://localhost:8000. Deberías ver:

    Hello from Docker! I have been seen 1 time(s).

    Actualiza la página: el contador se incrementa con cada visita.

    Esta configuración mínima funciona, pero presenta dos problemas que solucionarás en los siguientes pasos:

    • Carrera de inicio (startup race): web se inicia al mismo tiempo que redis. Si Redis aún no está listo, la aplicación Flask falla al conectarse y se bloquea.
    • Sin persistencia: Si ejecutas docker compose down seguido de docker compose up, el contador se restablece a cero. docker compose down elimina los contenedores y, con ellos, cualquier dato escrito en la capa de escritura del contenedor. docker compose stop conserva los contenedores para que los datos sobrevivan, pero no puedes confiar en eso en producción, donde los contenedores se reemplazan con regularidad.
  4. Detén el stack antes de continuar:

    $ docker compose down
    

Paso 3: Solucionar la carrera de inicio con comprobaciones de estado

Para solucionar la carrera de inicio, Compose debe esperar hasta que se confirme que redis está en buen estado (healthy) antes de iniciar web.

  1. Actualiza compose.yaml:

    services:
      web:
        build: .
        ports:
          - "${APP_PORT}:5000"
        environment:
          - REDIS_HOST=${REDIS_HOST}
          - REDIS_PORT=${REDIS_PORT}
        depends_on:
          redis:
            condition: service_healthy
    
      redis:
        image: redis:alpine
        healthcheck:
          test: ["CMD", "redis-cli", "ping"]
          interval: 5s
          timeout: 3s
          retries: 5
          start_period: 10s

    El bloque healthcheck le indica a Compose cómo comprobar si Redis está listo:

    • test es el comando que Compose ejecuta dentro del contenedor para verificar su estado. redis-cli ping se conecta a Redis y espera una respuesta PONG: si la obtiene, el contenedor está en buen estado.
    • start_period le otorga a Redis 10 segundos para inicializarse antes de que comiencen las comprobaciones de estado. Cualquier fallo durante este intervalo no cuenta para el límite de reintentos.
    • interval ejecuta la comprobación cada 5 segundos una vez transcurrido el periodo de inicio.
    • timeout otorga a cada comprobación 3 segundos para responder antes de considerarla un fallo.
    • retries establece cuántos fallos consecutivos se permiten antes de que Compose marque el contenedor como no saludable. Con interval: 5s y retries: 5, Compose esperará hasta 25 segundos antes de rendirse.
  2. Inicia el stack para confirmar que el orden se ha solucionado:

    $ docker compose up
    

    Deberías ver algo similar a:

    [+] Running 2/2
    ✔ Container compose-demo-redis-1  Healthy                       0.0s
  3. Abre http://localhost:8000 para confirmar que la aplicación sigue funcionando, luego detén el stack antes de continuar:

    $ docker compose down
    

Paso 4: Habilitar Compose Watch para actualizaciones en vivo

Sin Compose Watch, cada cambio de código requiere detener el stack, reconstruir la imagen y reiniciar los contenedores. Compose Watch elimina ese ciclo al sincronizar automáticamente los cambios en tu contenedor en ejecución a medida que guardas los archivos.

  1. Actualiza compose.yaml para añadir el bloque develop.watch al servicio web:

    services:
      web:
        build: .
        ports:
          - "${APP_PORT}:5000"
        environment:
          - REDIS_HOST=${REDIS_HOST}
          - REDIS_PORT=${REDIS_PORT}
        depends_on:
          redis:
            condition: service_healthy
        develop:
          watch:
            - action: sync+restart
              path: .
              target: /code
            - action: rebuild
              path: requirements.txt
    
      redis:
        image: redis:alpine
        healthcheck:
          test: ["CMD", "redis-cli", "ping"]
          interval: 5s
          timeout: 3s
          retries: 5
          start_period: 10s

    El bloque watch define dos reglas:

    • La acción sync+restart vigila el directorio de tu proyecto (.) en el host. Cuando un archivo cambia, Compose copia los archivos modificados en /code dentro del contenedor en ejecución y luego reinicia el contenedor. Debido a que el contenedor se reinicia con los archivos actualizados ya en su lugar, Flask se inicia leyendo el nuevo código directamente, sin necesidad de una reconstrucción o reinicio manual.
    • La acción rebuild en requirements.txt desencadena una reconstrucción completa de la imagen cada vez que añades una nueva dependencia, ya que instalar paquetes requiere reconstruir la imagen y no consiste únicamente en sincronizar archivos.
  2. Inicia el stack con Watch habilitado:

    $ docker compose up --watch
    
  3. Realiza un cambio en vivo. Abre app.py y actualiza el saludo:

    return f"Hello from Compose Watch! I have been seen {count} time(s).\n"
  4. Guarda el archivo. Compose Watch detecta el cambio y lo sincroniza de inmediato:

    Syncing service "web" after changes were detected
  5. Actualiza http://localhost:8000. El saludo actualizado aparece sin ningún reinicio y el contador debería seguir incrementándose.

  6. Detén el stack antes de continuar:

    $ docker compose down
    

    Para obtener más información sobre cómo funciona Compose Watch, consulta Usar Compose Watch.

Paso 5: Persistir datos con volúmenes con nombre

Cada vez que detienes y reinicias el stack, el contador de visitas se restablece a cero. Los datos de Redis viven dentro del contenedor, por lo que desaparecen cuando este se elimina. Un volumen con nombre soluciona esto almacenando los datos en el host, fuera del ciclo de vida del contenedor.

  1. Update compose.yaml:

    services:
      web:
        build: .
        ports:
          - "${APP_PORT}:5000"
        environment:
          - REDIS_HOST=${REDIS_HOST}
          - REDIS_PORT=${REDIS_PORT}
        depends_on:
          redis:
            condition: service_healthy
        develop:
          watch:
            - action: sync+restart
              path: .
              target: /code
            - action: rebuild
              path: requirements.txt
    
      redis:
        image: redis:alpine
        volumes:
          - redis-data:/data
        healthcheck:
          test: ["CMD", "redis-cli", "ping"]
          interval: 5s
          timeout: 3s
          retries: 5
          start_period: 10s
    
    volumes:
      redis-data:

    La entrada redis-data:/data bajo redis.volumes monta el volumen con nombre en /data, la ruta donde Redis escribe sus archivos de datos. La clave volumes de nivel superior lo registra con Docker para que persista entre los ciclos de compose down y compose up.

  2. Inicia el stack con docker compose up --watch y actualiza http://localhost:8000 unas cuantas veces para aumentar el contador.

  3. Destruye el stack con docker compose down y luego vuelve a levantarlo con docker compose up --watch.

  4. Abre http://localhost:8000: el contador continúa desde donde se quedó.

  5. Ahora restablece el contador con docker compose down -v.

    La bandera -v elimina los volúmenes con nombre junto con los contenedores. Utilízala de forma intencionada: elimina permanentemente los datos almacenados.

Paso 6: Estructurar tu proyecto con múltiples archivos de Compose

A medida que las aplicaciones crecen, un único archivo compose.yaml se vuelve más difícil de mantener. El elemento de nivel superior include te permite dividir los servicios en múltiples archivos mientras continúan formando parte de la misma aplicación.

Esto es especialmente útil cuando diferentes equipos son propietarios de distintas partes del stack, o cuando quieres reutilizar definiciones de infraestructura en varios proyectos.

  1. Crea un nuevo archivo en el directorio de tu proyecto llamado infra.yaml y mueve el servicio Redis y el volumen dentro de él:

     services:
      redis:
        image: redis:alpine
        volumes:
          - redis-data:/data
        healthcheck:
          test: ["CMD", "redis-cli", "ping"]
          interval: 5s
          timeout: 3s
          retries: 5
          start_period: 10s
    
    volumes:
      redis-data:
  2. Actualiza compose.yaml para incluir infra.yaml:

    include:
      - path: ./infra.yaml
    services:
      web:
        build: .
        ports:
          - "${APP_PORT}:5000"
        environment:
          - REDIS_HOST=${REDIS_HOST}
          - REDIS_PORT=${REDIS_PORT}
        depends_on:
          redis:
            condition: service_healthy
        develop:
          watch:
            - action: sync+restart
              path: .
              target: /code
            - action: rebuild
              path: requirements.txt
  3. Ejecuta la aplicación para confirmar que todo sigue funcionando:

    $ docker compose up --watch
    

    Compose fusiona ambos archivos en el inicio. El servicio web aún puede hacer referencia a redis por su nombre porque todos los servicios incluidos comparten la misma red predeterminada.

    Este es un ejemplo simplificado, pero demuestra el principio básico de include y cómo puede facilitar la modularización de aplicaciones complejas en archivos secundarios de Compose. Para obtener más información sobre include y cómo trabajar con múltiples archivos de Compose, consulta Trabajar con múltiples archivos de Compose.

  4. Detén el stack antes de continuar:

    $ docker compose down
    

Paso 7: Inspeccionar y depurar tu stack en ejecución

Con un stack completamente configurado, puedes observar lo que sucede dentro de tus contenedores sin detener nada. Este paso cubre los comandos principales para inspeccionar la configuración resuelta, transmitir logs y ejecutar comandos dentro de un contenedor en ejecución.

Antes de iniciar el stack, verifica que Compose haya resuelto tus variables de .env y fusionado todos los archivos correctamente:

$ docker compose config

docker compose config no requiere que el stack esté en ejecución: funciona exclusivamente a partir de tus archivos. Algunos detalles a observar en la salida:

  • ${APP_PORT}, ${REDIS_HOST} y ${REDIS_PORT} han sido reemplazados por los valores de tu archivo .env.
  • La notación corta de puertos ("8000:5000") se expande en sus campos canónicos (target, published, protocol).
  • Los nombres de la red y del volumen predeterminados se hacen explícitos, prefijados con el nombre del proyecto compose-demo.
  • La salida es la configuración completamente resuelta, con los archivos incorporados a través de include fusionados en una única vista.

Utiliza docker compose config en cualquier momento que quieras confirmar lo que Compose realmente aplicará, especialmente al depurar la sustitución de variables o al trabajar con múltiples archivos de Compose.

Ahora inicia el stack en modo detached (segundo plano) para que la terminal quede libre para los siguientes comandos:

$ docker compose up -d

Transmitir logs de todos los servicios

$ docker compose logs -f

La bandera -f sigue el flujo de logs en tiempo real, intercalando la salida de ambos contenedores con prefijos de nombre de servicio codificados por colores. Actualiza http://localhost:8000 unas cuantas veces y observa cómo aparecen los logs de solicitud de Flask. Para seguir los logs de un único servicio, pasa su nombre:

$ docker compose logs -f web

Presiona Ctrl+C para dejar de seguir los logs. Los contenedores continúan ejecutándose.

Ejecutar comandos dentro de un contenedor en ejecución

docker compose exec ejecuta un comando dentro de un contenedor que ya está en ejecución sin iniciar uno nuevo. Esta es la herramienta principal para la depuración en vivo.

Verificar que las variables de entorno estén configuradas correctamente

$ docker compose exec web env | grep REDIS
REDIS_HOST=redis
REDIS_PORT=6379

Probar que el contenedor web puede comunicarse con Redis utilizando el nombre del servicio como nombre de host

$ docker compose exec web python -c "import redis; r = redis.Redis(host='redis'); print(r.ping())"
True

Esto utiliza la misma biblioteca redis que usa tu aplicación, por lo que una respuesta True confirma que el descubrimiento de servicios, la red y la conexión a Redis están funcionando de extremo a extremo.

Inspeccionar el valor en tiempo real del contador de visitas en Redis

$ docker compose exec redis redis-cli GET hits

Siguientes pasos