Ir al contenido

Laboratorio 05 - Contenerización de GroceryListApp con Docker

En los laboratorios anteriores trabajó con imágenes ya construidas. En este laboratorio recorrerá el proceso anterior a docker run. Partirá del código fuente de una aplicación, definirá cómo debe construirse su imagen y ejecutará un contenedor creado a partir de esa imagen.

La aplicación utilizada es GroceryListApp, una aplicación web desarrollada con Flask que almacena su información en SQLite. El ejercicio permite relacionar el código fuente con el Dockerfile, el contexto de construcción, las capas de la imagen y la persistencia de datos.

Al finalizar podrá

  • clonar y reconocer la estructura básica de una aplicación web Flask;
  • crear un Dockerfile para construir una imagen reproducible;
  • ordenar las instrucciones del Dockerfile para aprovechar la caché de capas;
  • configurar .dockerignore para excluir archivos que no deben formar parte del contexto de construcción;
  • construir una imagen y asignarle una etiqueta de versión;
  • ejecutar un contenedor con publicación de puertos y almacenamiento persistente;
  • comprobar el funcionamiento de la aplicación mediante registros, una ruta de salud y la interfaz web;
  • comprobar que los datos sobreviven al reinicio del contenedor.

Compruebe que dispone de los siguientes elementos.

Requisito Comprobación
Docker instalado docker --version
Docker en ejecución docker info
Conexión a internet Necesaria para descargar imágenes y dependencias
Puerto 8080 disponible lsof -i :8080 no debería mostrar procesos escuchando en ese puerto
Navegador web Necesario para acceder a la aplicación

Verifique primero que Docker esté disponible.

Ventana de terminal
docker --version

Importante

Si el comando docker no es reconocido o devuelve errores de permisos, resuelva primero la instalación o configuración de Docker antes de continuar.

Descargue el repositorio de GroceryListApp.

Ventana de terminal
git clone https://github.com/jpadillaa/grocerylistapp.git

Ingrese al directorio del proyecto.

Ventana de terminal
cd grocerylistapp

Consulte su contenido.

Ventana de terminal
ls -la

La estructura debería ser similar a la siguiente.

.
├── app/
├── static/
├── templates/
├── tests/
├── requirements.txt
├── README.md
└── LICENSE

Antes de construir una imagen conviene reconocer qué archivos pertenecen a la aplicación y cuáles solo son necesarios durante desarrollo, pruebas o documentación. Esta distinción será importante cuando configure .dockerignore.

El Dockerfile describe las instrucciones que Docker utilizará para construir la imagen de la aplicación.

Cree el archivo en la raíz del proyecto.

Ventana de terminal
touch Dockerfile

Abra el archivo con su editor y agregue el siguiente contenido.

FROM python:3.11-slim
# Configurar directorio de trabajo
WORKDIR /app
# Instalar dependencias
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copiar el código de la aplicación
COPY . .
# Variables de entorno por defecto
ENV PORT=8080
ENV DATA_DIR=/data
ENV DB_PATH=/data/shop.db
ENV FLASK_CONFIG=app.config.Config
# Asegurar que el directorio de datos existe
RUN mkdir -p /data
# Exponer puerto
EXPOSE 8080
# El comando de arranque usa gunicorn
# La inicialización de la DB ocurre dentro del create_app() en app/__init__.py
CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT} \"app.main:app\""]

Cada instrucción modifica algún aspecto de la imagen que Docker está construyendo.

Instrucción Propósito
FROM python:3.11-slim Utiliza Python 3.11 sobre una variante reducida de la imagen base
WORKDIR /app Establece /app como directorio de trabajo para las instrucciones siguientes
COPY requirements.txt . Copia primero el archivo que declara las dependencias
RUN pip install --no-cache-dir -r requirements.txt Instala las dependencias de Python sin conservar la caché de pip
COPY . . Copia el resto del código de la aplicación
ENV ... Define valores de configuración disponibles dentro del contenedor
RUN mkdir -p /data Crea el directorio utilizado para almacenar la base de datos
EXPOSE 8080 Documenta el puerto utilizado por la aplicación
CMD [...] Define el comando que se ejecutará cuando inicie el contenedor

EXPOSE 8080 no publica el puerto en el sistema anfitrión. La publicación se realizará más adelante mediante docker run -p.

El orden de las instrucciones influye en la reutilización de la caché de construcción.

El archivo requirements.txt se copia antes que el resto del código y las dependencias se instalan inmediatamente después. Si el código cambia pero requirements.txt permanece igual, Docker puede reutilizar la capa donde instaló las dependencias.

La secuencia relevante es

requirements.txt
↓
instalación de dependencias
↓
código de la aplicación

Si COPY . . apareciera antes de instalar las dependencias, un cambio cualquiera en el código podría invalidar esa parte de la caché y obligar a repetir la instalación.

Cuando ejecuta docker build, Docker utiliza un conjunto de archivos como contexto de construcción. .dockerignore permite excluir contenido que no debe enviarse o incorporarse durante ese proceso.

Cree el archivo.

Ventana de terminal
touch .dockerignore

Agregue el siguiente contenido.

# Control de versiones
.git
.gitignore
# Entornos virtuales de Python
venv/
.venv/
__pycache__/
*.py[cod]
*.pyo
# Archivos de IDE/Editor
.vscode/
.idea/
*.swp
*.swo
.DS_Store
# Tests y documentación (no necesarios en producción)
tests/
*.md
!README.md
# Archivos de Docker (evita recursión)
Dockerfile
docker-compose*.yml
.dockerignore
# Datos locales (la DB debe persistir en volúmenes, no en la imagen)
data/
*.db
# Archivos de configuración local
.env
.env.*

Este archivo cumple varias funciones.

  • Reduce la cantidad de archivos incluidos en el contexto de construcción.
  • Evita incorporar artefactos locales que no necesita la aplicación.
  • Excluye archivos de datos que no deberían quedar almacenados dentro de la imagen.
  • Reduce el riesgo de incluir archivos locales de configuración como .env.
  • Evita que cambios en archivos irrelevantes invaliden innecesariamente la caché de construcción.

La relación entre ambos archivos puede entenderse así.

Directorio del proyecto
│
├── .dockerignore decide qué entra al contexto
│
▼
Contexto de construcción
│
├── Dockerfile define qué hacer con ese contenido
│
▼
Imagen Docker

Con el Dockerfile y .dockerignore preparados, construya la imagen.

Ventana de terminal
docker build -t grocerylistapp:1.0.0 .

Los dos elementos finales del comando tienen funciones diferentes.

Elemento Función
-t grocerylistapp:1.0.0 Asigna el nombre grocerylistapp y la etiqueta 1.0.0
. Utiliza el directorio actual como contexto de construcción

La etiqueta permite identificar una versión concreta de la imagen. En este ejercicio se utiliza 1.0.0 en lugar de depender únicamente de latest.

Ventana de terminal
docker images | grep grocerylistapp

Una salida aproximada puede verse así.

grocerylistapp 1.0.0 abc123def456 2 minutes ago 180MB

Hasta este punto Docker ha construido una imagen. Todavía no existe un contenedor ejecutando la aplicación.

Cree un contenedor a partir de la imagen recién construida.

Ventana de terminal
docker run -d \
--name grocerylist \
-p 8080:8080 \
-v grocerylist-data:/data \
grocerylistapp:1.0.0

El comando combina la ejecución de la aplicación con dos decisiones adicionales.

Elemento Función
-d Ejecuta el contenedor en segundo plano
--name grocerylist Asigna un nombre estable al contenedor
-p 8080:8080 Publica el puerto 8080 del contenedor mediante el puerto 8080 del sistema anfitrión
-v grocerylist-data:/data Monta el volumen nombrado grocerylist-data en /data
grocerylistapp:1.0.0 Indica la imagen utilizada para crear el contenedor

La aplicación utiliza /data/shop.db para almacenar la base de datos SQLite. Al montar grocerylist-data sobre /data, ese archivo queda asociado con el volumen y no únicamente con la capa escribible del contenedor.

Importante

El volumen permite conservar la base de datos más allá del ciclo de vida de un contenedor específico. Detener y volver a iniciar el mismo contenedor no elimina su capa escribible, pero eliminarlo sí hace necesario un mecanismo externo si los datos deben sobrevivir a la creación de una nueva instancia.

Ventana de terminal
docker ps

Una salida aproximada puede verse así.

CONTAINER ID IMAGE STATUS PORTS
a1b2c3d4e5f6 grocerylistapp:1.0.0 Up 30 seconds 0.0.0.0:8080->8080/tcp

No basta con confirmar que el contenedor aparece como Up. Conviene comprobar también que el proceso inició correctamente, que responde mediante HTTP y que la aplicación puede utilizar su almacenamiento.

Ventana de terminal
docker logs grocerylist

Busque mensajes similares a los siguientes.

[INFO] Starting gunicorn 21.x.x
[INFO] Listening at: http://0.0.0.0:8080

Estos mensajes indican que Gunicorn inició y está escuchando en el puerto esperado.

Ventana de terminal
curl http://localhost:8080/health

La respuesta esperada es

{"status":"ok"}

La ruta de salud proporciona una comprobación directa de que la aplicación está respondiendo.

Abra en el navegador

http://localhost:8080

Debería aparecer la interfaz de GroceryListApp.

Cree varios elementos desde la aplicación.

Detenga el contenedor.

Ventana de terminal
docker stop grocerylist

Inícielo nuevamente.

Ventana de terminal
docker start grocerylist

Abra otra vez la aplicación y compruebe que los elementos creados continúan disponibles.

Esta prueba confirma que la aplicación recupera la información almacenada en /data/shop.db después del reinicio.

El ejercicio realizado hasta este punto conecta varias etapas que conviene distinguir.

Código fuente
↓
.dockerignore
↓
Contexto de construcción
↓
Dockerfile
↓
docker build
↓
Imagen grocerylistapp:1.0.0
↓
docker run
↓
Contenedor grocerylist
├── puerto 8080
└── /data
↓
volumen grocerylist-data

Cada elemento cumple una responsabilidad diferente.

  • El código fuente contiene la aplicación.
  • .dockerignore controla qué archivos forman parte del contexto de construcción.
  • El Dockerfile describe cómo transformar ese contexto en una imagen.
  • docker build materializa la imagen.
  • docker run crea un contenedor a partir de esa imagen.
  • La publicación de puertos permite acceder al servicio desde el sistema anfitrión.
  • El volumen desacopla los datos persistentes del contenedor específico que ejecuta la aplicación.

Consulte primero sus registros.

Ventana de terminal
docker logs grocerylist --tail 50

El laboratorio original identifica los siguientes casos frecuentes.

Síntoma Posible causa Acción propuesta
ModuleNotFoundError Dependencias no instaladas correctamente Reconstruir la imagen sin reutilizar caché
Permission denied en /data Problema de acceso al directorio de datos Revisar la configuración del volumen
Address already in use Puerto 8080 ocupado Publicar la aplicación mediante otro puerto del host

Para reconstruir la imagen sin reutilizar la caché puede ejecutar

Ventana de terminal
docker build --no-cache -t grocerylistapp:1.0.0 .

Si el puerto 8080 está ocupado, puede utilizar otro puerto del sistema anfitrión.

Ventana de terminal
docker run -d -p 8081:8080 --name grocerylist grocerylistapp:1.0.0

Compruebe que el volumen existe.

Ventana de terminal
docker volume ls | grep grocerylist-data

Si no existe, puede crearlo explícitamente.

Ventana de terminal
docker volume create grocerylist-data

Después deberá ejecutar el contenedor con el montaje correspondiente.

Ventana de terminal
-v grocerylist-data:/data

La existencia del volumen por sí sola no hace que la aplicación lo utilice. El volumen debe estar montado en la ruta donde GroceryListApp almacena la base de datos.

Después de completar el laboratorio debería poder explicar la contenerización como una cadena de decisiones y no únicamente como la creación de un Dockerfile.

La imagen depende del contenido enviado durante la construcción y de las instrucciones declaradas en el Dockerfile. El orden de esas instrucciones afecta la reutilización de capas. .dockerignore determina qué archivos quedan fuera del contexto y el volumen permite separar los datos persistentes de la instancia concreta que ejecuta la aplicación.

También debería poder relacionar las principales operaciones utilizadas durante el ejercicio.

Acción Comando
Construir la imagen docker build -t grocerylistapp:1.0.0 .
Ejecutar el contenedor docker run -d --name grocerylist -p 8080:8080 -v grocerylist-data:/data grocerylistapp:1.0.0
Detener el contenedor docker stop grocerylist
Iniciar el contenedor docker start grocerylist
Reiniciar el contenedor docker restart grocerylist
Seguir los registros docker logs -f grocerylist
Acceder al shell docker exec -it grocerylist /bin/bash
Eliminar el contenedor docker rm -f grocerylist
Eliminar la imagen docker rmi grocerylistapp:1.0.0
Eliminar el volumen docker volume rm grocerylist-data

Advertencia

Eliminar grocerylist-data elimina también los datos persistidos en ese volumen. Hágalo únicamente cuando ya no necesite conservar la información.

Fuente original

Laboratorio 05 - Contenerización de GroceryListApp con Docker