Técnicas Avanzadas de Dockerfile

DockerBeginner
Practicar Ahora

Introducción

En esta práctica de laboratorio, profundizaremos en las técnicas de Dockerfile, explorando conceptos avanzados que te ayudarán a crear imágenes de Docker más eficientes y flexibles. Cubriremos instrucciones detalladas de Dockerfile, compilaciones multietapa (multi-stage builds) y el uso de archivos .dockerignore. También exploraremos el concepto crucial de las capas (layers) en las imágenes de Docker. Al finalizar esta sesión, tendrás una comprensión integral de estas técnicas avanzadas y podrás aplicarlas en tus propios proyectos.

Este laboratorio está diseñado pensando en principiantes, proporcionando explicaciones detalladas y abordando posibles puntos de confusión. Utilizaremos WebIDE (VS Code) para todas nuestras tareas de edición de archivos, lo que facilita la creación y modificación de archivos directamente en el navegador.

Comprensión de las Instrucciones y Capas de Dockerfile

Comencemos creando un Dockerfile que utilice varias instrucciones. Construiremos una imagen para una aplicación web Python usando Flask y, en el proceso, exploraremos cómo cada instrucción contribuye a las capas de nuestra imagen de Docker.

  1. Primero, vamos a crear un nuevo directorio para nuestro proyecto. En la terminal del WebIDE, ejecuta:
mkdir -p ~/project/advanced-dockerfile && cd ~/project/advanced-dockerfile

Este comando crea una nueva carpeta llamada advanced-dockerfile dentro de la carpeta project y luego se sitúa dentro de ella.

  1. Ahora, creemos nuestro archivo de aplicación. En el explorador de archivos del WebIDE (normalmente a la izquierda), haz clic derecho en la carpeta advanced-dockerfile y selecciona "New File". Nombra este archivo como app.py.

  2. Abre app.py y añade el siguiente código Python:

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from {os.environ.get('ENVIRONMENT', 'unknown')} environment!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Esta es una aplicación Flask sencilla que responde con un mensaje de saludo, incluyendo el entorno en el que se está ejecutando.

  1. A continuación, necesitamos crear un archivo requirements.txt para especificar nuestras dependencias de Python. Crea un nuevo archivo llamado requirements.txt en el mismo directorio y añade el siguiente contenido:
Flask==2.0.1
Werkzeug==2.0.1

Aquí estamos especificando versiones exactas tanto para Flask como para Werkzeug para asegurar la compatibilidad.

  1. Ahora, vamos a crear nuestro Dockerfile. Crea un nuevo archivo llamado Dockerfile (con 'D' mayúscula) en el mismo directorio y añade el siguiente contenido:
## Use an official Python runtime as the base image
FROM python:3.9-slim

## Set the working directory in the container
WORKDIR /app

## Set an environment variable
ENV ENVIRONMENT=production

## Copy the requirements file into the container
COPY requirements.txt .

## Install the required packages
RUN pip install --no-cache-dir -r requirements.txt

## Copy the application code into the container
COPY app.py .

## Specify the command to run when the container starts
CMD ["python", "app.py"]

## Expose the port the app runs on
EXPOSE 5000

## Add labels for metadata
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo for advanced Dockerfile techniques"

Ahora, analicemos estas instrucciones y comprendamos cómo contribuyen a las capas de nuestra imagen:

  • FROM python:3.9-slim: Esta es siempre la primera instrucción. Especifica la imagen base sobre la que construimos. Esto crea la primera capa de nuestra imagen, que incluye el entorno de ejecución de Python.
  • WORKDIR /app: Establece el directorio de trabajo para las instrucciones posteriores. No crea una capa nueva, pero afecta el comportamiento de las siguientes instrucciones.
  • ENV ENVIRONMENT=production: Define una variable de entorno. Las variables de entorno no crean capas nuevas, sino que se almacenan en los metadatos de la imagen.
  • COPY requirements.txt .: Copia el archivo de requisitos desde nuestro host hacia la imagen. Esto crea una nueva capa que contiene solo este archivo.
  • RUN pip install --no-cache-dir -r requirements.txt: Ejecuta un comando en el contenedor durante el proceso de construcción. Instala nuestras dependencias de Python. Esto crea una nueva capa que contiene todos los paquetes instalados.
  • COPY app.py .: Copia el código de nuestra aplicación en la imagen, creando otra capa.
  • CMD ["python", "app.py"]: Especifica el comando que se ejecutará cuando el contenedor se inicie. No crea una capa, sino que define el comando por defecto.
  • EXPOSE 5000: En realidad, es solo una forma de documentación. Indica a Docker que el contenedor escuchará en este puerto en tiempo de ejecución, pero no publica el puerto realmente. No crea una capa.
  • LABEL ...: Añaden metadatos a la imagen. Al igual que las instrucciones ENV, no crean capas nuevas, sino que se guardan en los metadatos.

Cada instrucción RUN, COPY y ADD en un Dockerfile crea una nueva capa. Las capas son un concepto fundamental en Docker que permite el almacenamiento y la transferencia eficiente de imágenes. Cuando realizas cambios en tu Dockerfile y vuelves a construir la imagen, Docker reutilizará las capas en caché que no hayan cambiado, acelerando el proceso de construcción.

  1. Ahora que entendemos qué hace nuestro Dockerfile, construyamos la imagen. En la terminal, ejecuta:
docker build -t advanced-flask-app .

Este comando construye una nueva imagen de Docker con la etiqueta advanced-flask-app. El . al final le indica a Docker que busque el Dockerfile en el directorio actual.

Verás una salida que muestra cada paso del proceso. Observa cómo cada paso corresponde a una instrucción en nuestro Dockerfile y cómo Docker menciona "Using cache" para los pasos que no han cambiado si ejecutas el comando de construcción varias veces.

  1. Una vez finalizada la construcción, podemos ejecutar un contenedor basado en nuestra nueva imagen:
docker run -d -p 5000:5000 --name flask-container advanced-flask-app

Este comando hace lo siguiente:

  • -d ejecuta el contenedor en modo desatendido (en segundo plano).
  • -p 5000:5000 mapea el puerto 5000 de tu host al puerto 5000 del contenedor.
  • --name flask-container asigna un nombre a nuestro nuevo contenedor.
  • advanced-flask-app es la imagen que usamos para crear el contenedor.

Puedes verificar que el contenedor está funcionando consultando la lista de contenedores activos:

docker ps
  1. Para probar si nuestra aplicación funciona correctamente, podemos usar el comando curl:
curl http://localhost:5000

Deberías ver el mensaje "Hello from production environment!"

Si tienes problemas con curl, también puedes abrir una nueva pestaña en el navegador y visitar http://localhost:5000. Deberías ver el mismo mensaje.

Si encuentras algún problema, puedes revisar los registros (logs) del contenedor usando:

docker logs flask-container

Esto te mostrará cualquier mensaje de error o salida de tu aplicación Flask.

Compilaciones Multietapa (Multi-stage Builds)

Ahora que comprendemos las instrucciones básicas y las capas, exploremos una técnica más avanzada: las compilaciones multietapa. Estas permiten usar múltiples sentencias FROM en un solo Dockerfile. Es particularmente útil para crear imágenes finales más pequeñas al copiar solo los artefactos necesarios de una etapa a otra.

Modifiquemos nuestro Dockerfile para usar una compilación multietapa que resulte en una imagen más ligera:

  1. En el WebIDE, abre el Dockerfile que creamos anteriormente.
  2. Reemplaza todo el contenido con lo siguiente:
## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .

RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

WORKDIR /app

## Copy only the installed packages from the builder stage
COPY --from=builder /root/.local /root/.local
COPY app.py .

ENV PATH=/root/.local/bin:$PATH
ENV ENVIRONMENT=production

CMD ["python", "app.py"]

EXPOSE 5000

LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo with multi-stage build"

Analicemos qué está sucediendo en este Dockerfile multietapa:

  1. Comenzamos con una etapa de construcción llamada builder:

    • Usamos la imagen Python 3.9-slim como base para mantener la ligereza desde el inicio.
    • Instalamos nuestras dependencias de Python en esta etapa usando pip install --user. Esto instala los paquetes en el directorio personal del usuario.
  2. Luego tenemos nuestra etapa final:

    • Empezamos de cero con otra imagen Python 3.9-slim.
    • Copiamos solo los paquetes instalados desde la etapa builder, específicamente desde /root/.local donde pip install --user los ubicó.
    • Copiamos el código de nuestra aplicación.
    • Añadimos el directorio bin local al PATH para que Python pueda encontrar los paquetes instalados.
    • Configuramos el resto de nuestro contenedor (ENV, CMD, EXPOSE, LABEL) como antes.

La ventaja clave aquí es que nuestra imagen final no incluye ninguna de las herramientas de construcción ni las cachés del proceso de instalación de pip. Solo contiene los artefactos finales necesarios, lo que resulta en una imagen más pequeña.

  1. Construyamos esta nueva imagen multietapa. En la terminal, ejecuta:
docker build -t multi-stage-flask-app .
  1. Una vez terminada la construcción, comparemos los tamaños de nuestras dos imágenes. Ejecuta:
docker images | grep flask-app
multi-stage-flask-app         latest     7bdd1be2d1fb   10 seconds ago   129MB
advanced-flask-app            latest     c59d6fa303cc   10 minutes ago   136MB

Deberías notar que multi-stage-flask-app es más pequeña que la advanced-flask-app que construimos al principio.

  1. Ahora, ejecutemos un contenedor con nuestra nueva imagen optimizada:
docker run -d -p 5001:5000 --name multi-stage-container multi-stage-flask-app

Ten en cuenta que estamos usando un puerto de host diferente (5001) para evitar conflictos con nuestro contenedor anterior.

  1. Prueba la aplicación:
curl http://localhost:5001

Deberías seguir viendo el mensaje "Hello from production environment!"

  1. Para profundizar en las diferencias entre nuestras imágenes de etapa única y multietapa, podemos usar el comando docker history. Ejecuta estos comandos:
docker history advanced-flask-app
docker history multi-stage-flask-app

Compara los resultados. Notarás que la compilación multietapa tiene menos capas y tamaños más reducidos en algunas de ellas.

Las compilaciones multietapa son una técnica poderosa para crear imágenes de Docker eficientes. Te permiten usar herramientas y archivos en tu proceso de construcción sin engrosar tu imagen final. Esto es especialmente útil para lenguajes compilados o aplicaciones con procesos de construcción complejos.

En este caso, la hemos usado para crear una imagen de aplicación Python más pequeña al copiar solo los paquetes instalados y el código necesarios, dejando atrás cualquier residuo de construcción o caché.

Uso del Archivo .dockerignore

Al construir una imagen de Docker, el cliente envía todos los archivos del directorio al demonio de Docker. Si tienes archivos grandes que no son necesarios para construir la imagen, esto puede ralentizar el proceso. El archivo .dockerignore permite especificar qué archivos y directorios deben excluirse al construir una imagen.

Creemos un archivo .dockerignore y veamos cómo funciona:

  1. En el WebIDE, crea un nuevo archivo en el directorio advanced-dockerfile y nómbralo .dockerignore.
  2. Añade el siguiente contenido al archivo .dockerignore:
**/.git
**/.gitignore
**/__pycache__
**/*.pyc
**/*.pyo
**/*.pyd
**/.Python
**/env
**/venv
**/ENV
**/env.bak
**/venv.bak

Desglosemos qué significan estos patrones:

  • **/.git: Ignora el directorio .git y todo su contenido, donde sea que aparezca en la estructura de carpetas.
  • **/.gitignore: Ignora los archivos .gitignore.
  • **/__pycache__: Ignora los directorios de caché de Python.
  • **/*.pyc, **/*.pyo, **/*.pyd: Ignora archivos compilados de Python.
  • **/.Python: Ignora archivos .Python (a menudo creados por entornos virtuales).
  • **/env, **/venv, **/ENV: Ignora directorios de entornos virtuales.
  • **/env.bak, **/venv.bak: Ignora copias de seguridad de directorios de entornos virtuales.

El prefijo ** al inicio de cada línea significa "en cualquier directorio".

  1. Para demostrar el efecto del archivo .dockerignore, vamos a crear algunos archivos que queremos ignorar. En la terminal, ejecuta:
mkdir venv
touch venv/ignore_me.txt
touch .gitignore

Estos comandos crean un directorio venv con un archivo dentro y un archivo .gitignore. Estos son elementos comunes en proyectos de Python que normalmente no queremos dentro de nuestras imágenes de Docker.

  1. Ahora, construye la imagen de nuevo:
docker build -t ignored-flask-app .
  1. Para verificar que los archivos ignorados no se incluyeron en el contexto de construcción, podemos usar el comando docker history:
docker history ignored-flask-app

No deberías ver ningún paso que copie el directorio venv o el archivo .gitignore.

El archivo .dockerignore es una herramienta fundamental para mantener tus imágenes de Docker limpias y tu proceso de construcción eficiente. Es especialmente útil en proyectos grandes donde podrías tener muchos archivos que no se requieren en la imagen final.

Instrucciones Avanzadas de Dockerfile

En este paso final, exploraremos algunas instrucciones adicionales de Dockerfile y mejores prácticas que ayudarán a que tus imágenes sean más seguras, fáciles de mantener y de usar. También nos enfocaremos en la resolución de problemas y la verificación de cada paso.

  1. En el WebIDE, abre el Dockerfile nuevamente.

  2. Reemplaza el contenido con lo siguiente:

## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

## Create a non-root user
RUN useradd -m appuser

## Install curl for healthcheck
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

WORKDIR /app

## Dynamically determine Python version and site-packages path
RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && \
    SITE_PACKAGES_PATH="/home/appuser/.local/lib/python${PYTHON_VERSION}/site-packages" && \
    mkdir -p "${SITE_PACKAGES_PATH}" && \
    chown -R appuser:appuser /home/appuser/.local

## Copy site-packages and binaries using the variable
COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}"
COPY --from=builder /root/.local/bin /home/appuser/.local/bin
COPY app.py .

ENV PATH=/home/appuser/.local/bin:$PATH
ENV ENVIRONMENT=production

## Set the user to run the application
USER appuser

## Use ENTRYPOINT with CMD
ENTRYPOINT ["python"]
CMD ["app.py"]

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:5000/ || exit 1

ARG BUILD_VERSION
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="${BUILD_VERSION:-1.0}"
LABEL description="Flask app demo with advanced Dockerfile techniques"

Analicemos los nuevos conceptos introducidos:

  • RUN useradd -m appuser: Crea un nuevo usuario llamado appuser. Ejecutar aplicaciones como un usuario sin privilegios de root es una mejor práctica de seguridad, ya que limita el daño potencial si la aplicación se ve comprometida. La bandera -m crea un directorio home para el usuario.
  • RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*: Instala el paquete curl, necesario para que nuestra instrucción HEALTHCHECK funcione. También limpiamos la caché de apt para reducir el tamaño de la imagen.
  • RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && ...: Este conjunto de comandos determina dinámicamente la versión de Python dentro del contenedor y crea el directorio site-packages correcto para el appuser. También establece los permisos adecuados.
  • COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}": Copia los paquetes instalados desde la etapa builder a la ruta de site-packages determinada dinámicamente, asegurando que los paquetes estén en el lugar correcto para que appuser los use.
  • COPY --from=builder /root/.local/bin /home/appuser/.local/bin: Copia los scripts ejecutables instalados por pip al directorio bin local del appuser.
  • ENTRYPOINT ["python"] con CMD ["app.py"]: Cuando se usan juntos, ENTRYPOINT define el ejecutable principal (en este caso, python) y CMD proporciona los argumentos por defecto (app.py). Este patrón ofrece flexibilidad: los usuarios pueden ejecutar el contenedor y lanzar app.py por defecto, o pueden sobrescribir el CMD para ejecutar otros scripts o comandos de Python.
  • HEALTHCHECK: Configura una prueba de salud para el contenedor. Docker ejecutará periódicamente el comando especificado (curl -f http://localhost:5000/) para determinar si el contenedor está sano. Si el comando falla, el contenedor se considera "unhealthy".
  • ARG BUILD_VERSION: Define un argumento de construcción llamado BUILD_VERSION. Los argumentos de construcción permiten pasar valores a la imagen en el momento de la creación.
  • LABEL version="${BUILD_VERSION:-1.0}": Establece una etiqueta de versión. Utiliza el argumento BUILD_VERSION; si no se proporciona durante la construcción, se usará 1.0 por defecto.
  1. Ahora, construyamos esta nueva imagen especificando una versión de construcción:
docker build -t advanced-flask-app-v2 --build-arg BUILD_VERSION=2.0 .

La bandera --build-arg BUILD_VERSION=2.0 nos permite pasar el valor 2.0 al argumento de construcción durante el proceso.

  1. Una vez finalizada la construcción, verifiquemos que la imagen se creó correctamente:
docker images | grep advanced-flask-app-v2
  1. Ahora, ejecutemos un contenedor con la nueva imagen:
docker run -d -p 5002:5000 --name advanced-container-v2 advanced-flask-app-v2
  1. Verifiquemos que el contenedor está funcionando:
docker ps | grep advanced-container-v2

Si no ves el contenedor en la lista, es posible que haya finalizado inesperadamente. Puedes revisar todos los contenedores (incluidos los detenidos) con:

docker ps -a | grep advanced-container-v2

Si el estado no es "Up", revisa los logs para diagnosticar el problema:

docker logs advanced-container-v2
  1. Suponiendo que el contenedor está en ejecución, tras darle un momento para iniciar, podemos verificar su estado de salud:
docker inspect --format='{{.State.Health.Status}}' advanced-container-v2

Tras un breve retraso, deberías ver "healthy". Si aparece "unhealthy" inicialmente, espera otros 30 segundos y vuelve a intentarlo.

  1. También podemos verificar que nuestra etiqueta de versión se aplicó correctamente:
docker inspect -f '{{.Config.Labels.version}}' advanced-container-v2

Deberías ver "2.0" como salida.

  1. Finalmente, probemos nuestra aplicación enviando una solicitud:
curl http://localhost:5002

Deberías ver el mensaje "Hello from production environment!". Esto indica que tu aplicación Flask se está ejecutando correctamente dentro del contenedor y es accesible a través del puerto 5002 de tu host.

Estas técnicas avanzadas te permiten crear imágenes de Docker más seguras, configurables y listas para producción. El uso de un usuario no raíz mejora la seguridad, el HEALTHCHECK ayuda en la orquestación y monitoreo, y los argumentos de construcción permiten una creación de imágenes más flexible y versionada.

Resumen

En este laboratorio, exploramos técnicas avanzadas de Dockerfile que te ayudarán a crear imágenes de Docker más eficientes, seguras y fáciles de mantener. Cubrimos:

  1. Instrucciones detalladas de Dockerfile y su impacto en las capas de la imagen: Aprendimos cómo cada instrucción contribuye a la estructura de nuestra imagen y cómo entender las capas nos ayuda a optimizarlas.
  2. Compilaciones multietapa (Multi-stage builds): Utilizamos esta técnica para crear imágenes finales más pequeñas separando nuestro entorno de construcción del entorno de ejecución.
  3. Uso de archivos .dockerignore: Aprendimos a excluir archivos innecesarios del contexto de construcción, lo que acelera el proceso y reduce el tamaño de la imagen.
  4. Instrucciones avanzadas de Dockerfile: Exploramos instrucciones adicionales como USER, ENTRYPOINT, HEALTHCHECK y ARG, que nos permiten crear imágenes más seguras y flexibles.

Estas técnicas te permiten:

  • Crear imágenes de Docker más optimizadas y pequeñas.
  • Mejorar la seguridad ejecutando aplicaciones como usuarios sin privilegios de root.
  • Implementar comprobaciones de salud para una mejor orquestación de contenedores.
  • Utilizar variables en tiempo de construcción para una creación de imágenes más flexible.

A lo largo de este laboratorio, utilizamos WebIDE (VS Code) para editar nuestros archivos, facilitando la creación y modificación de Dockerfiles y código de aplicación directamente en el navegador. Este enfoque permite una experiencia de desarrollo fluida al trabajar con Docker.