Ir al contenido

Laboratorio 07 - Aplicación multicontenedor con Docker Compose

Hasta este punto ha ejecutado contenedores de manera individual mediante docker run. En este laboratorio trabajará con una aplicación compuesta por varios servicios que deben comunicarse y coordinar su ciclo de vida.

Utilizará Docker Compose para definir y operar dos servicios de GroceryListApp. El servicio app ejecutará la aplicación Flask y el servicio web utilizará Nginx como punto de entrada. Ambos compartirán una red creada por Compose y la aplicación conservará sus datos en un volumen nombrado.

Servicio Imagen o construcción Responsabilidad
app Imagen construida a partir del Dockerfile del repositorio Ejecutar GroceryListApp y almacenar sus datos SQLite en /data
web nginx:alpine Recibir las solicitudes externas y reenviarlas al servicio app

El flujo de una solicitud puede representarse así.

Navegador
│
│ http://localhost
▼
Puerto 80 del sistema anfitrión
│
▼
Servicio web
Nginx
│
│ http://app:8080
▼
Servicio app
GroceryListApp
│
▼
/data/shop.db
│
▼
Volumen grocerylist-data

Solo web publica un puerto hacia el sistema anfitrión. El servicio app permanece accesible dentro de la red del proyecto y Nginx se comunica con él mediante su nombre de servicio.

Al finalizar podrá

  • definir una aplicación multicontenedor mediante Docker Compose;
  • construir una imagen desde un Dockerfile como parte de un proyecto Compose;
  • declarar servicios, volúmenes y dependencias en docker-compose.yml;
  • configurar Nginx como proxy inverso hacia un servicio interno;
  • utilizar nombres de servicio para la comunicación entre contenedores;
  • comprobar el estado de salud de una aplicación antes de iniciar un servicio dependiente;
  • observar la red creada automáticamente por Compose;
  • consultar registros y ejecutar comandos en servicios administrados por Compose;
  • comprobar la persistencia de datos entre ciclos de down y up.

Necesitará

  • Docker y Docker Compose instalados y en ejecución;
  • el puerto 80 disponible en su equipo;
  • una terminal con acceso a Docker;
  • un navegador web.

Este laboratorio parte del repositorio utilizado en los ejercicios anteriores.

Clone el repositorio si todavía no lo tiene disponible.

Ventana de terminal
git clone https://github.com/jpadillaa/grocerylistapp.git
cd grocerylistapp
mkdir -p nginx

Después de agregar los archivos de este laboratorio, la estructura relevante será similar a la siguiente.

grocerylistapp/
├── app/
├── static/
├── templates/
├── tests/
├── nginx/
│ └── default.conf
├── requirements.txt
├── Dockerfile
└── docker-compose.yml

El repositorio ya contiene el Dockerfile necesario para construir la imagen de la aplicación.

Nginx recibirá las solicitudes externas y las reenviará al servicio app.

Cree el archivo nginx/default.conf.

server {
listen 80;
location / {
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

La instrucción central es

proxy_pass http://app:8080;

app no es una dirección IP escrita de forma manual. Es el nombre del servicio que definirá posteriormente en Compose. Los contenedores conectados a la red del proyecto pueden utilizar esos nombres para encontrarse entre sí.

El puerto 8080 corresponde al puerto donde GroceryListApp escucha dentro del contenedor.

Cree docker-compose.yml en la raíz del proyecto.

services:
app:
build: .
volumes:
- grocerylist-data:/data
healthcheck:
test: ["CMD-SHELL", "curl --fail http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 5s
start_period: 5s
retries: 3
restart: unless-stopped
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
app:
condition: service_healthy
restart: unless-stopped
volumes:
grocerylist-data:

Antes de crear los contenedores, pida a Compose que procese y valide la configuración.

Ventana de terminal
docker compose config

Si el archivo es válido, Compose mostrará la configuración resultante.

La definición comienza con

app:
build: .

build: . indica que Compose debe construir la imagen utilizando el contexto del directorio actual y el Dockerfile disponible allí.

El servicio no declara ports. Por esta razón, el ejercicio no publica directamente el puerto 8080 hacia el sistema anfitrión.

La persistencia se configura mediante

volumes:
- grocerylist-data:/data

GroceryListApp almacena su base de datos SQLite en /data. El volumen permite que esos datos tengan un ciclo de vida independiente de un contenedor específico.

El servicio incluye además una comprobación de salud.

healthcheck:
test: ["CMD-SHELL", "curl --fail http://localhost:8080/health || exit 1"]
interval: 10s
timeout: 5s
start_period: 5s
retries: 3

Compose puede utilizar ese estado para determinar cuándo la aplicación está lista para aceptar solicitudes.

El segundo servicio utiliza una imagen existente.

web:
image: nginx:alpine

Solo este servicio publica un puerto.

ports:
- "80:80"

El archivo de configuración de Nginx se monta en modo de solo lectura.

volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro

La dependencia con app se expresa mediante

depends_on:
app:
condition: service_healthy

En esta configuración, Compose espera a que app alcance el estado de salud esperado antes de iniciar web.

Esto reduce la posibilidad de que Nginx comience a recibir tráfico mientras GroceryListApp todavía se encuentra en su proceso inicial de arranque.

El archivo no declara una sección networks.

Compose crea automáticamente una red para el proyecto y conecta a ella los servicios definidos. Esa red permite que web pueda resolver el nombre app sin conocer de antemano la dirección IP concreta del contenedor.

La sección final

volumes:
grocerylist-data:

declara el volumen nombrado utilizado por app.

La arquitectura declarada puede resumirse así.

Proyecto Compose
│
├── Red del proyecto
│ ├── app
│ └── web
│
└── Volumen
└── grocerylist-data

Ejecute

Ventana de terminal
docker compose up -d

Compose realizará varias acciones a partir de la declaración del proyecto.

  • Construirá la imagen de app.
  • Descargará nginx:alpine si todavía no está disponible localmente.
  • Creará la red del proyecto.
  • Creará el volumen grocerylist-data.
  • Creará los contenedores correspondientes.
  • Iniciará app.
  • Evaluará su comprobación de salud.
  • Iniciará web de acuerdo con la dependencia declarada.

Compruebe el estado.

Ventana de terminal
docker compose ps

Una salida aproximada puede verse así.

NAME SERVICE STATUS PORTS
grocerylistapp-app-1 app running (healthy)
grocerylistapp-web-1 web running 0.0.0.0:80->80/tcp

app debería aparecer en ejecución y con estado saludable. web debería aparecer en ejecución y con el puerto 80 publicado.

Importante

Si un servicio aparece como exited o restarting, consulte sus registros antes de continuar.

Ventana de terminal
docker compose logs <servicio>

La primera prueba consulta la ruta de salud desde el sistema anfitrión.

Ventana de terminal
curl http://localhost/health

La respuesta esperada es

{"status":"ok"}

La solicitud entra por el servicio web y Nginx la reenvía a app.

Abra después

http://localhost

Debería aparecer la interfaz de GroceryListApp.

Realice algunas operaciones.

  1. Cree una categoría desde /categories.
  2. Agregue elementos desde /add.
  3. Marque un elemento como completado desde la pantalla principal.
  4. Recargue la página y compruebe que los cambios permanecen disponibles.

También puede consultar únicamente los encabezados HTTP.

Ventana de terminal
curl -I http://localhost

Una respuesta aproximada puede verse así.

HTTP/1.1 200 OK
Server: nginx/...
Content-Type: text/html; charset=utf-8

El encabezado Server permite observar que la respuesta pasa por Nginx antes de llegar al cliente.

Compose crea una red para que los servicios puedan comunicarse.

Ventana de terminal
docker network ls | grep grocerylistapp

En este proyecto debería aparecer una red similar a

grocerylistapp_default
Ventana de terminal
docker network inspect grocerylistapp_default

Revise la sección Containers. Allí deberían aparecer los contenedores correspondientes a app y web, cada uno con una dirección dentro de la red.

Desde el servicio web, intente resolver app.

Ventana de terminal
docker compose exec web sh -c "nslookup app 2>/dev/null || ping -c 1 -W 2 app"

La prueba permite observar que la comunicación no depende de escribir manualmente la dirección IP del contenedor.

Nginx utiliza

app

como nombre estable del servicio dentro del proyecto.

Comprobar que app no está publicado directamente

Sección titulada «Comprobar que app no está publicado directamente»

Ejecute desde el sistema anfitrión

Ventana de terminal
curl -s --max-time 3 http://localhost:8080 || echo "No accesible desde el host (esperado)"

El fallo de conexión es el resultado esperado en esta configuración.

app participa en la red interna, pero no tiene una publicación de puertos hacia el host. El punto de entrada externo es web.

Este patrón permite concentrar en Nginx responsabilidades como el enrutamiento hacia la aplicación. En arquitecturas más amplias, un proxy inverso también puede participar en funciones adicionales, según los requisitos del sistema.

Compose permite operar los servicios como parte de un mismo proyecto.

Ventana de terminal
docker compose logs --tail 10
Ventana de terminal
docker compose logs app

En una terminal ejecute

Ventana de terminal
docker compose logs -f web

Desde otra terminal o desde el navegador acceda a

http://localhost

Las nuevas solicitudes deberían aparecer en los registros de web.

Finalice el seguimiento con Ctrl + C.

Compruebe que la base de datos existe.

Ventana de terminal
docker compose exec app ls -la /data/

Debería aparecer shop.db.

Puede consultar las tablas mediante Python.

Ventana de terminal
docker compose exec app python -c "
import sqlite3
conn = sqlite3.connect('/data/shop.db')
tables = conn.execute(\"SELECT name FROM sqlite_master WHERE type='table'\").fetchall()
print('Tablas:', [t[0] for t in tables])
conn.close()
"

docker compose exec utiliza el nombre lógico del servicio. No necesita conocer el nombre generado para el contenedor.

Hasta este punto el volumen existe, pero conviene comprobar que realmente desacopla los datos del ciclo de vida de los contenedores.

Ejecute

Ventana de terminal
docker compose down

Este comando elimina los contenedores y la red creada para la aplicación, pero no elimina por defecto el volumen nombrado.

Ventana de terminal
docker volume ls | grep grocerylist-data

Debería seguir apareciendo un volumen con un nombre similar a

grocerylistapp_grocerylist-data
Ventana de terminal
docker compose up -d

Abra nuevamente

http://localhost

Los elementos y categorías creados anteriormente deberían continuar disponibles.

La prueba demuestra esta separación.

docker compose down
│
├── elimina contenedores
├── elimina la red del proyecto
└── conserva el volumen
│
▼
docker compose up -d
│
└── nuevos contenedores reutilizan los datos

Comprobar qué ocurre al eliminar el volumen

Sección titulada «Comprobar qué ocurre al eliminar el volumen»

El siguiente comando cambia deliberadamente el resultado.

Ventana de terminal
docker compose down -v

La opción -v solicita eliminar también los volúmenes nombrados definidos por el proyecto.

Si después ejecuta

Ventana de terminal
docker compose up -d

GroceryListApp comenzará con una base de datos nueva.

Advertencia

docker compose down -v elimina los volúmenes del proyecto y, con ellos, los datos almacenados allí. Utilícelo únicamente cuando realmente quiera descartar ese estado.

Cuando ya no necesite conservar la aplicación ni sus datos, ejecute

Ventana de terminal
docker compose down -v

Compruebe que el volumen desapareció.

Ventana de terminal
docker volume ls | grep grocerylist-data

Si no hay coincidencias, el volumen ya no está disponible.

Compruebe primero el estado de los servicios.

Ventana de terminal
docker compose ps

Después consulte los registros de app.

Ventana de terminal
docker compose logs app

Un 502 en este ejercicio indica que Nginx está disponible, pero no pudo obtener una respuesta válida del servicio al que intenta reenviar la solicitud.

Los datos desaparecen al recrear los contenedores

Sección titulada «Los datos desaparecen al recrear los contenedores»

Compruebe los montajes del contenedor de app.

Ventana de terminal
docker inspect grocerylistapp-app-1 --format '{{json .Mounts}}' | python3 -m json.tool

Busque un montaje asociado con /data.

Si el nombre real del contenedor difiere del ejemplo, puede identificarlo previamente con

Ventana de terminal
docker compose ps

Compruebe qué proceso utiliza el puerto.

Ventana de terminal
lsof -i :80

También puede cambiar únicamente el puerto del sistema anfitrión.

ports:
- "8888:80"

En ese caso, acceda mediante

http://localhost:8888

El servicio Nginx continúa escuchando en el puerto 80 dentro del contenedor.

Ejecute

Ventana de terminal
docker compose config

Compose procesará el archivo y señalará problemas de sintaxis o estructura YAML.

Revise especialmente la indentación. YAML depende de ella para representar la jerarquía del documento.

Después de completar el laboratorio debería poder explicar qué aporta Compose frente a una sucesión de comandos docker run.

El archivo docker-compose.yml declara los servicios que forman la aplicación y las relaciones necesarias para operarlos conjuntamente. En este ejercicio, app se construye localmente, web utiliza una imagen existente, ambos comparten una red del proyecto y app utiliza un volumen persistente.

También debería poder distinguir cuatro relaciones.

  • Exposición externa — solo web publica un puerto hacia el sistema anfitrión.
  • Comunicación interna — web encuentra a app mediante el nombre de servicio.
  • Dependencia de inicio — web depende del estado de salud de app.
  • Persistencia — el volumen grocerylist-data conserva la base de datos fuera del ciclo de vida de los contenedores.

La arquitectura final puede resumirse así.

Host
│
│ puerto 80
▼
web
Nginx
│
│ red Compose
▼
app
Flask
│
│ /data
▼
grocerylist-data
Acción Comando
Validar la configuración docker compose config
Crear e iniciar los servicios docker compose up -d
Consultar su estado docker compose ps
Consultar registros docker compose logs
Seguir los registros de un servicio docker compose logs -f web
Ejecutar un comando docker compose exec app <comando>
Detener y eliminar contenedores docker compose down
Eliminar también los volúmenes docker compose down -v
Recurso Enlace
Docker Compose Documentación de Docker Compose
Compose file Referencia del archivo Compose
Redes en Compose Networking in Compose
GroceryListApp Repositorio en GitHub
Nginx Imagen oficial en Docker Hub

Fuente original

Laboratorio 07 - Aplicación multicontenedor con Docker Compose