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:
- Instalada la última versión de Docker Compose
- Una comprensión básica de los conceptos de Docker y de cómo funciona Docker
Paso 1: Configurar el proyecto
Crea un directorio para el proyecto:
$ mkdir compose-demo $ cd compose-demoCrea el archivo
app.pyen 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.
Crea el archivo
requirements.txten el directorio de tu proyecto y añade lo siguiente:flask redisCrea 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`ImportantAsegúrate de que el archivo se llame
Dockerfilesin extensión. Algunos editores añaden.txtautomá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.
Crea un archivo
.envpara almacenar los valores de configuración:APP_PORT=8000 REDIS_HOST=redis REDIS_PORT=6379Compose lee automáticamente el archivo
.envy hace que estos valores estén disponibles para su interpolación en tu archivocompose.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
Crea un archivo
.dockerignorepara mantener los archivos innecesarios fuera del contexto de construcción:.env *.pyc __pycache__ redis-dataDocker 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.
Crea el archivo
compose.yamlen 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:alpineEste archivo de Compose define dos servicios:
El servicio
webutiliza una imagen que se construye a partir delDockerfileen el directorio actual. Mapea el puerto8000del host al puerto5000del contenedor donde Flask escucha de forma predeterminada.El servicio
redisutiliza 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.Inicia tu aplicación:
$ docker compose upCon 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.
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):
webse inicia al mismo tiempo queredis. Si Redis aún no está listo, la aplicación Flask falla al conectarse y se bloquea. - Sin persistencia: Si ejecutas
docker compose downseguido dedocker compose up, el contador se restablece a cero.docker compose downelimina los contenedores y, con ellos, cualquier dato escrito en la capa de escritura del contenedor.docker compose stopconserva los contenedores para que los datos sobrevivan, pero no puedes confiar en eso en producción, donde los contenedores se reemplazan con regularidad.
- Carrera de inicio (startup race):
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.
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: 10sEl bloque
healthcheckle indica a Compose cómo comprobar si Redis está listo:testes el comando que Compose ejecuta dentro del contenedor para verificar su estado.redis-cli pingse conecta a Redis y espera una respuestaPONG: si la obtiene, el contenedor está en buen estado.start_periodle 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.intervalejecuta la comprobación cada 5 segundos una vez transcurrido el periodo de inicio.timeoutotorga a cada comprobación 3 segundos para responder antes de considerarla un fallo.retriesestablece cuántos fallos consecutivos se permiten antes de que Compose marque el contenedor como no saludable. Coninterval: 5syretries: 5, Compose esperará hasta 25 segundos antes de rendirse.
Inicia el stack para confirmar que el orden se ha solucionado:
$ docker compose upDeberías ver algo similar a:
[+] Running 2/2 ✔ Container compose-demo-redis-1 Healthy 0.0sAbre
http://localhost:8000para 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.
Actualiza
compose.yamlpara añadir el bloquedevelop.watchal servicioweb: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: 10sEl bloque
watchdefine dos reglas:- La acción
sync+restartvigila el directorio de tu proyecto (.) en el host. Cuando un archivo cambia, Compose copia los archivos modificados en/codedentro 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
rebuildenrequirements.txtdesencadena 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.
- La acción
Inicia el stack con Watch habilitado:
$ docker compose up --watchRealiza un cambio en vivo. Abre
app.pyy actualiza el saludo:return f"Hello from Compose Watch! I have been seen {count} time(s).\n"Guarda el archivo. Compose Watch detecta el cambio y lo sincroniza de inmediato:
Syncing service "web" after changes were detectedActualiza
http://localhost:8000. El saludo actualizado aparece sin ningún reinicio y el contador debería seguir incrementándose.Detén el stack antes de continuar:
$ docker compose downPara 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.
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:/databajoredis.volumesmonta el volumen con nombre en/data, la ruta donde Redis escribe sus archivos de datos. La clavevolumesde nivel superior lo registra con Docker para que persista entre los ciclos decompose downycompose up.Inicia el stack con
docker compose up --watchy actualizahttp://localhost:8000unas cuantas veces para aumentar el contador.Destruye el stack con
docker compose downy luego vuelve a levantarlo condocker compose up --watch.Abre
http://localhost:8000: el contador continúa desde donde se quedó.Ahora restablece el contador con
docker compose down -v.La bandera
-velimina 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.
Crea un nuevo archivo en el directorio de tu proyecto llamado
infra.yamly 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:Actualiza
compose.yamlpara incluirinfra.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.txtEjecuta la aplicación para confirmar que todo sigue funcionando:
$ docker compose up --watchCompose fusiona ambos archivos en el inicio. El servicio
webaún puede hacer referencia aredispor su nombre porque todos los servicios incluidos comparten la misma red predeterminada.Este es un ejemplo simplificado, pero demuestra el principio básico de
includey cómo puede facilitar la modularización de aplicaciones complejas en archivos secundarios de Compose. Para obtener más información sobreincludey cómo trabajar con múltiples archivos de Compose, consulta Trabajar con múltiples archivos de Compose.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
includefusionados 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=6379Probar 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())"
TrueEsto 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