Laboratorio 06 - Contenerización de GroceryListApp con Dockerfile optimizado
En el Laboratorio 05 construyó una imagen funcional de GroceryListApp a partir de su código fuente. En este laboratorio partirá de esa base y modificará el Dockerfile para incorporar decisiones orientadas a seguridad, reproducibilidad y operación.
El ejercicio introduce una construcción multietapa, un usuario sin privilegios de root, Tini como proceso de inicialización, una comprobación de salud definida en la imagen, metadatos OCI y restricciones aplicadas al ejecutar el contenedor.
El objetivo no es únicamente producir una imagen diferente. Cada cambio permite observar una responsabilidad concreta del empaquetado o de la ejecución del contenedor.
Lo que aprenderá
Sección titulada «Lo que aprenderá»Al finalizar podrá
- separar la construcción de dependencias de la imagen utilizada durante la ejecución;
- utilizar una versión específica de la imagen base para hacer más explícita la dependencia del entorno;
- ejecutar la aplicación con un usuario sin privilegios de
root; - utilizar Tini para manejar señales y procesos hijos;
- definir una comprobación de salud mediante
HEALTHCHECK; - incorporar metadatos OCI a la imagen;
- aplicar límites de CPU y memoria, un sistema de archivos de solo lectura y restricciones de privilegios;
- comprobar que los controles configurados se aplican durante la ejecución;
- comparar el Dockerfile del Laboratorio 05 con la nueva versión.
Antes de comenzar
Sección titulada «Antes de comenzar»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 paquetes |
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 que Docker esté disponible.
docker --versionImportante
Si el comando
dockerno es reconocido o devuelve errores de permisos, resuelva primero la instalación o configuración de Docker antes de continuar.
Obtener el repositorio
Sección titulada «Obtener el repositorio»Omita esta sección si ya tiene el repositorio utilizado en el Laboratorio 05.
git clone https://github.com/jpadillaa/grocerylistapp.gitcd grocerylistappCrear el Dockerfile
Sección titulada «Crear el Dockerfile»Cree el archivo Dockerfile en la raíz del proyecto.
touch DockerfileReemplace su contenido por la siguiente versión.
# ============================================================================# ETAPA 1: Build - Instalación de dependencias# ============================================================================FROM python:3.11.9-slim-bookworm AS builder
# Evitar prompts interactivos y configurar PythonENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1
WORKDIR /build
# Instalar dependencias en un virtualenv para multi-stage limpioRUN python -m venv /opt/venvENV PATH="/opt/venv/bin:$PATH"
# Copiar solo requirements para aprovechar caché de capasCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt
# ============================================================================# ETAPA 2: Runtime - Imagen final optimizada# ============================================================================FROM python:3.11.9-slim-bookworm AS runtime
# ------------------------------------------------------------------------------# Metadatos de la imagen (OCI Image Spec)# ------------------------------------------------------------------------------LABEL org.opencontainers.image.title="GroceryListApp" \ org.opencontainers.image.description="Aplicación web para gestionar listas de compras" \ org.opencontainers.image.version="1.0.0" \ org.opencontainers.image.vendor="jpadillaa" \ org.opencontainers.image.source="https://github.com/jpadillaa/grocerylistapp" \ org.opencontainers.image.licenses="GPL-3.0"
# ------------------------------------------------------------------------------# Variables de entorno# ------------------------------------------------------------------------------ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ # Configuración de la aplicación PORT=8080 \ DATA_DIR=/data \ DB_PATH=/data/shop.db \ FLASK_CONFIG=app.config.Config \ # Ruta del virtualenv PATH="/opt/venv/bin:$PATH"
# ------------------------------------------------------------------------------# Instalación de dependencias del sistema y configuración de seguridad# ------------------------------------------------------------------------------RUN set -eux; \ # Instalar tini para manejo correcto de señales apt-get update; \ apt-get install -y --no-install-recommends \ tini \ curl \ ; \ # Limpiar caché de apt para reducir tamaño apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ rm -rf /var/lib/apt/lists/*; \ # Crear usuario no-root para seguridad groupadd --gid 1000 appgroup; \ useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser; \ # Crear directorio de datos con permisos correctos mkdir -p /data; \ chown -R appuser:appgroup /data
# ------------------------------------------------------------------------------# Copiar dependencias desde la etapa de build# ------------------------------------------------------------------------------COPY --from=builder /opt/venv /opt/venv
# ------------------------------------------------------------------------------# Configurar directorio de trabajo y copiar código fuente# ------------------------------------------------------------------------------WORKDIR /app
# Copiar código con ownership del usuario no-rootCOPY --chown=appuser:appgroup . .
# ------------------------------------------------------------------------------# Configuración de seguridad final# ------------------------------------------------------------------------------# Cambiar a usuario no-rootUSER appuser
# Declarar volumen para persistenciaVOLUME ["/data"]
# Exponer puerto (documentación)EXPOSE 8080
# Health check para orquestadoresHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl --fail http://localhost:${PORT}/health || exit 1
# ------------------------------------------------------------------------------# Punto de entrada y comando# ------------------------------------------------------------------------------# Usar tini como init system para manejo correcto de señales y procesos zombieENTRYPOINT ["/usr/bin/tini", "--"]
# Comando de arranque con gunicornCMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT} --workers 2 --threads 4 --worker-class gthread --access-logfile - --error-logfile - \"app.main:app\""]La nueva versión incorpora decisiones que pertenecen a momentos diferentes del ciclo de vida.
Construcción │ ├── versión explícita de la imagen base ├── etapa builder └── instalación de dependencias │ ▼Imagen de ejecución │ ├── usuario no root ├── Tini ├── metadatos OCI ├── HEALTHCHECK └── configuración de Gunicorn │ ▼docker run │ ├── límites de CPU y memoria ├── sistema de archivos de solo lectura ├── no-new-privileges └── volumen persistenteEsta separación será útil durante las siguientes secciones porque permite distinguir qué controles quedan incorporados en la imagen y cuáles se aplican cuando se crea el contenedor.
Separar construcción y ejecución
Sección titulada «Separar construcción y ejecución»El Dockerfile utiliza dos instrucciones FROM con nombres diferentes.
FROM python:3.11.9-slim-bookworm AS builder# ... instalación de dependencias ...
FROM python:3.11.9-slim-bookworm AS runtimeCOPY --from=builder /opt/venv /opt/venvLa etapa builder prepara el entorno virtual e instala las dependencias. La etapa runtime parte nuevamente de la imagen base y recibe únicamente el entorno virtual resultante.
| Aspecto | Aporte |
|---|---|
| Separación de etapas | Distingue los artefactos necesarios durante la construcción de los necesarios durante la ejecución |
| Imagen final | Solo incorpora desde builder el entorno virtual preparado |
| Superficie disponible | Evita trasladar automáticamente a la etapa final todo lo que pudiera haberse utilizado durante la construcción |
En este ejercicio ambas etapas utilizan la misma imagen base y el ahorro de tamaño puede ser moderado. La propia comparación del laboratorio muestra que el aporte principal de la construcción multietapa está en la separación de responsabilidades.
Utilizar una versión específica de la imagen base
Sección titulada «Utilizar una versión específica de la imagen base»El Dockerfile anterior utilizaba
FROM python:3.11-slimLa nueva versión utiliza
FROM python:3.11.9-slim-bookwormLa referencia más específica hace explícitas tanto la versión de Python como la variante de Debian utilizada como base.
| Elemento | Propósito |
|---|---|
3.11.9 |
Identifica una versión concreta de Python |
bookworm |
Identifica la variante de Debian utilizada por la imagen |
Esta decisión reduce la ambigüedad respecto de una referencia más general como python:3.11-slim.
Configurar Python y pip
Sección titulada «Configurar Python y pip»La etapa de construcción define las siguientes variables.
ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1| Variable | Efecto esperado en este ejercicio |
|---|---|
PYTHONDONTWRITEBYTECODE=1 |
Evita generar archivos .pyc |
PYTHONUNBUFFERED=1 |
Hace que la salida de Python se escriba sin búfer y aparezca oportunamente en los registros |
PIP_NO_CACHE_DIR=1 |
Evita conservar la caché de pip |
PIP_DISABLE_PIP_VERSION_CHECK=1 |
Evita la comprobación de nuevas versiones de pip durante la construcción |
La intención es reducir archivos que no necesita la imagen y hacer más directa la salida de la aplicación hacia los registros del contenedor.
Consolidar instrucciones RUN
Sección titulada «Consolidar instrucciones RUN»El laboratorio contrasta varias instrucciones independientes
RUN apt-get updateRUN apt-get install -y tiniRUN rm -rf /var/lib/apt/lists/*con una única instrucción.
RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends tini curl; \ rm -rf /var/lib/apt/lists/*La limpieza ocurre dentro de la misma instrucción que instala los paquetes. De este modo, los archivos temporales eliminados no quedan conservados en una capa anterior de esa misma secuencia.
set -eux modifica el comportamiento del shell utilizado durante la construcción.
| Opción | Efecto |
|---|---|
-e |
Detiene la ejecución cuando un comando falla |
-u |
Trata como error el uso de variables no definidas |
-x |
Muestra los comandos que se ejecutan |
Ejecutar con un usuario sin privilegios de root
Sección titulada «Ejecutar con un usuario sin privilegios de root»El Dockerfile crea un grupo y un usuario dedicados a la aplicación.
RUN groupadd --gid 1000 appgroup; \ useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser
USER appuserLa instrucción USER appuser establece qué usuario ejecutará las instrucciones posteriores y el proceso principal del contenedor.
El propósito es aplicar el principio de mínimo privilegio. Si la aplicación no necesita permisos administrativos para funcionar, ejecutarla con un usuario dedicado reduce las operaciones que puede realizar dentro del contenedor.
El directorio /data se prepara antes del cambio de usuario.
mkdir -p /data; \chown -R appuser:appgroup /dataEsto permite que appuser escriba en la ubicación donde SQLite almacena la base de datos.
Utilizar Tini para manejar señales y procesos hijos
Sección titulada «Utilizar Tini para manejar señales y procesos hijos»La imagen instala Tini y lo define como punto de entrada.
RUN apt-get install -y --no-install-recommends tini
ENTRYPOINT ["/usr/bin/tini", "--"]Tini se convierte en el proceso que ocupa el PID 1 y ejecuta el comando definido posteriormente mediante CMD.
| Situación | Función que cumple Tini en el ejercicio |
|---|---|
| Recepción de señales | Reenvía señales como SIGTERM y SIGINT al proceso hijo |
| Procesos hijos finalizados | Puede recolectar procesos que de otro modo quedarían como zombis |
| Detención del contenedor | Facilita que Gunicorn reciba la señal de terminación y cierre de forma ordenada |
La relación entre los procesos queda aproximadamente así.
PID 1Tini │ ▼sh -c │ ▼Gunicorn │ └── workersDefinir una comprobación de salud
Sección titulada «Definir una comprobación de salud»El Dockerfile incorpora
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl --fail http://localhost:${PORT}/health || exit 1Docker ejecutará periódicamente la solicitud indicada y mantendrá un estado de salud asociado con el contenedor.
| Parámetro | Valor | Función |
|---|---|---|
--interval |
30s |
Intervalo entre comprobaciones |
--timeout |
10s |
Tiempo máximo permitido para una comprobación |
--start-period |
5s |
Periodo inicial antes de contabilizar fallos |
--retries |
3 |
Número de fallos consecutivos antes de marcar el contenedor como unhealthy |
La comprobación utiliza la ruta /health de GroceryListApp. Por esta razón, su utilidad depende también de que esa ruta represente adecuadamente el estado que se desea observar.
El laboratorio menciona que otros sistemas de ejecución y orquestación disponen de mecanismos propios para trabajar con comprobaciones de salud. La configuración concreta depende de cada plataforma.
Incorporar metadatos OCI
Sección titulada «Incorporar metadatos OCI»La imagen incluye etiquetas bajo el espacio de nombres org.opencontainers.image.*.
LABEL org.opencontainers.image.title="GroceryListApp" \ org.opencontainers.image.description="Aplicación web para gestionar listas de compras" \ org.opencontainers.image.version="1.0.0" \ org.opencontainers.image.vendor="jpadillaa" \ org.opencontainers.image.source="https://github.com/jpadillaa/grocerylistapp" \ org.opencontainers.image.licenses="GPL-3.0"Estas etiquetas almacenan metadatos junto con la imagen y pueden consultarse posteriormente mediante docker inspect.
En este caso documentan el nombre, la descripción, la versión, el proveedor, el repositorio de origen y la licencia declarada por la imagen.
Configurar Gunicorn
Sección titulada «Configurar Gunicorn»El comando final utiliza una configuración explícita de Gunicorn.
CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT} --workers 2 --threads 4 --worker-class gthread --access-logfile - --error-logfile - \"app.main:app\""]| Opción | Valor | Función en esta configuración |
|---|---|---|
--workers |
2 |
Inicia dos procesos de trabajo |
--threads |
4 |
Permite hasta cuatro hilos por worker |
--worker-class |
gthread |
Utiliza el worker basado en hilos de Gunicorn |
--access-logfile - |
stdout |
Envía los registros de acceso a la salida estándar |
--error-logfile - |
stderr |
Envía los errores a la salida de error |
Los valores elegidos forman parte de la configuración de este ejercicio. Su ajuste en otros entornos depende de la carga, los recursos disponibles y el comportamiento de la aplicación.
Construir la imagen
Sección titulada «Construir la imagen»Construya la nueva imagen con dos etiquetas.
docker build \ --tag grocerylistapp:1.0.0 \ --tag grocerylistapp:latest \ --build-arg BUILDKIT_INLINE_CACHE=1 \ .| Opción | Propósito dentro del laboratorio |
|---|---|
--tag grocerylistapp:1.0.0 |
Asigna una etiqueta de versión |
--tag grocerylistapp:latest |
Asigna una segunda etiqueta de conveniencia |
--build-arg BUILDKIT_INLINE_CACHE=1 |
Configura el argumento utilizado en el ejercicio para caché de construcción |
Compruebe el tamaño de la imagen.
docker images grocerylistapp:1.0.0 --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"Ejecutar el contenedor con restricciones adicionales
Sección titulada «Ejecutar el contenedor con restricciones adicionales»Ejecute la imagen mediante el siguiente comando.
docker run -d \ --name grocerylist \ --restart unless-stopped \ --memory 256m \ --cpus 0.5 \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,size=64m \ --security-opt no-new-privileges:true \ -p 8080:8080 \ -v grocerylist-data:/data \ grocerylistapp:1.0.0La construcción de la imagen y la ejecución del contenedor resuelven responsabilidades diferentes. Las siguientes opciones no quedan almacenadas en el Dockerfile, sino que se aplican a esta instancia durante docker run.
| Opción | Efecto esperado |
|---|---|
--restart unless-stopped |
Configura una política de reinicio automático salvo detención manual |
--memory 256m |
Limita la memoria disponible para el contenedor |
--cpus 0.5 |
Limita la capacidad de CPU disponible |
--read-only |
Monta el sistema de archivos raíz del contenedor como solo lectura |
--tmpfs /tmp:rw,noexec,nosuid,size=64m |
Proporciona un espacio temporal escribible en memoria |
--security-opt no-new-privileges:true |
Impide que los procesos adquieran privilegios adicionales |
-p 8080:8080 |
Publica el puerto utilizado por la aplicación |
-v grocerylist-data:/data |
Proporciona almacenamiento escribible y persistente para SQLite |
--read-only cambia un supuesto importante de la ejecución. Las rutas que deban aceptar escritura tienen que proporcionarse explícitamente. En este ejercicio /data se monta mediante un volumen y /tmp mediante tmpfs.
Sistema de archivos raíz └── solo lectura
/data └── volumen grocerylist-data
/tmp └── tmpfsComprobar los controles configurados
Sección titulada «Comprobar los controles configurados»La verificación no se limita a comprobar que la aplicación responde. Cada prueba siguiente busca evidencia de que una decisión concreta quedó aplicada.
Comprobar el estado de salud
Sección titulada «Comprobar el estado de salud»Espere aproximadamente 30 segundos después del inicio y ejecute
docker inspect grocerylist --format='{{.State.Health.Status}}'La salida esperada es
healthyPuede consultar el historial completo con
docker inspect grocerylist --format='{{json .State.Health}}' | jqComprobar el usuario
Sección titulada «Comprobar el usuario»docker exec grocerylist whoamiLa salida esperada es
appuserEsto comprueba que el proceso ejecutado dentro del contenedor utiliza el usuario definido en el Dockerfile.
Comprobar el sistema de archivos de solo lectura
Sección titulada «Comprobar el sistema de archivos de solo lectura»Intente crear un archivo en /app.
docker exec grocerylist touch /app/test.txtLa operación debería fallar con un mensaje similar al siguiente.
touch: cannot touch '/app/test.txt': Read-only file systemEl fallo es la evidencia esperada en esta prueba.
Comprobar la ruta de salud
Sección titulada «Comprobar la ruta de salud»curl -s http://localhost:8080/health | jqLa respuesta esperada es
{ "status": "ok"}Consultar los metadatos de la imagen
Sección titulada «Consultar los metadatos de la imagen»docker inspect grocerylistapp:1.0.0 --format='{{json .Config.Labels}}' | jqLa salida debería incluir las etiquetas org.opencontainers.image.* declaradas en el Dockerfile.
Comparar con el Laboratorio 05
Sección titulada «Comparar con el Laboratorio 05»El cambio entre ambos laboratorios no consiste en una sola optimización. Se incorporan controles y responsabilidades que antes no estaban explícitos.
| Aspecto | Laboratorio 05 | Laboratorio 06 |
|---|---|---|
| Construcción multietapa | No | builder y runtime |
| Usuario | root |
appuser con UID 1000 |
| Imagen base | python:3.11-slim |
python:3.11.9-slim-bookworm |
| Proceso de inicialización | Ninguno explícito | Tini |
| Comprobación de salud | No | HEALTHCHECK |
| Metadatos OCI | No | Etiquetas estándar |
| Configuración de Gunicorn | Básica | Workers, hilos y registros explícitos |
| Destino de registros | Configuración por defecto | stdout y stderr |
La ejecución también incorpora controles adicionales.
| Control | Laboratorio 05 | Laboratorio 06 |
|---|---|---|
Usuario sin privilegios de root |
No | Sí |
| Sistema de archivos raíz de solo lectura | No | Sí |
| Límites de recursos | No | Sí |
no-new-privileges |
No | Sí |
| Política de reinicio | No | Sí |
El laboratorio propone además la siguiente comparación aproximada de tamaño.
| Versión | Tamaño aproximado |
|---|---|
| Dockerfile del Laboratorio 05 | ~180–200 MB |
| Dockerfile del Laboratorio 06 | ~150–170 MB |
Estos valores son estimaciones del ejercicio y pueden variar según las versiones de las imágenes y las dependencias utilizadas.
En este caso la construcción multietapa no introduce herramientas de compilación pesadas que luego desaparezcan de la imagen final. Por eso, según el propio laboratorio, su aporte principal está en la separación de responsabilidades y no necesariamente en una reducción drástica del tamaño.
Solución de problemas
Sección titulada «Solución de problemas»La comprobación de salud falla
Sección titulada «La comprobación de salud falla»Consulte la última ejecución registrada.
docker inspect grocerylist --format='{{json .State.Health}}' | jq '.Log[-1]'El laboratorio propone revisar los siguientes casos.
| Síntoma | Posible causa | Acción propuesta |
|---|---|---|
connection refused |
La aplicación todavía no acepta conexiones | Aumentar --start-period |
curl not found |
curl no está disponible en la imagen |
Revisar su instalación en el Dockerfile |
404 en /health |
La ruta no existe o no está disponible | Revisar la definición de la ruta en la aplicación |
Aparecen errores de permisos en /data
Sección titulada «Aparecen errores de permisos en /data»Compruebe los permisos almacenados en el volumen.
docker run --rm -v grocerylist-data:/data alpine ls -la /dataEl laboratorio propone corregir la propiedad de los archivos con
docker run --rm -v grocerylist-data:/data alpine chown -R 1000:1000 /dataEste comando modifica los propietarios de los archivos contenidos en el volumen y debe utilizarse únicamente cuando esa sea la corrección requerida.
El contenedor fue terminado por falta de memoria
Sección titulada «El contenedor fue terminado por falta de memoria»Compruebe el indicador correspondiente.
docker inspect grocerylist --format='{{.State.OOMKilled}}'El laboratorio propone aumentar el límite de memoria con
docker update --memory 512m grocerylistGunicorn no responde a SIGTERM
Sección titulada «Gunicorn no responde a SIGTERM»Compruebe los procesos del contenedor.
docker exec grocerylist ps auxLa salida esperada en el laboratorio es similar a
PID USER COMMAND1 appuser /usr/bin/tini -- sh -c gunicorn ...7 appuser gunicorn: master [app.main:app]La comprobación busca confirmar que Tini ocupa el PID 1 y que Gunicorn se ejecuta como proceso descendiente.
Lo que debe poder reconocer ahora
Sección titulada «Lo que debe poder reconocer ahora»El Dockerfile de este laboratorio incorpora controles en tres niveles diferentes.
El primer nivel corresponde a la construcción de la imagen. Aquí aparecen la separación entre builder y runtime, la versión explícita de la imagen base, la caché de capas y la copia selectiva de dependencias.
El segundo corresponde a la configuración de la imagen de ejecución. Aquí aparecen el usuario appuser, Tini, HEALTHCHECK, los metadatos OCI y la configuración de Gunicorn.
El tercero corresponde a la creación del contenedor. Aquí se aplican los límites de CPU y memoria, el sistema de archivos de solo lectura, tmpfs, no-new-privileges, la publicación de puertos y el volumen persistente.
Distinguir estos niveles permite razonar con mayor precisión sobre dónde debe implementarse cada control y qué evidencia permite comprobarlo.
Comandos de referencia
Sección titulada «Comandos de referencia»| Acción | Comando |
|---|---|
| Consultar el estado de salud | docker inspect grocerylist --format='{{.State.Health.Status}}' |
| Consultar el uso de recursos | docker stats grocerylist --no-stream |
| Seguir los registros | docker logs -f grocerylist |
| Abrir un shell | docker exec -it grocerylist /bin/bash |
| Escanear vulnerabilidades | docker scout cves grocerylistapp:1.0.0 |
| Consultar las capas de la imagen | docker history grocerylistapp:1.0.0 |
Fuente original
Laboratorio 06 - Contenerización de GroceryListApp con Dockerfile optimizado