6. Creación de imágenes personalizadas con Dockerfile

El verdadero poder de Docker se libera cuando dejas de usar solo imágenes preexistentes y empiezas a crear las tuyas. Esto te permite empaquetar tus propias aplicaciones y dependencias. La herramienta para esto es el Dockerfile.

Estructura de un Dockerfile

Un Dockerfile es un script que contiene una serie de instrucciones para ensamblar una imagen. Se lee de arriba hacia abajo, y cada instrucción crea una nueva capa en la imagen.

Preparando los archivos en Windows

Para este ejemplo, necesitarás una carpeta que contenga tres archivos. Si usas Windows, el proceso es el siguiente:

  1. Crea una carpeta para el proyecto: Usa el Explorador de Archivos para crear una carpeta nueva en una ubicación que recuerdes, como C:\proyectos\mi-app-flask. Toda la operación se realizará dentro de esta carpeta.
  2. Crea los archivos: Dentro de la carpeta que acabas de crear, tienes que añadir los tres archivos. Puedes usar un editor de código como Visual Studio Code o incluso el Bloc de notas.
    • app.py: El código de tu aplicación.
    • requirements.txt: Las dependencias de Python.
    • Dockerfile: Las instrucciones de Docker.
  3. ¡Atención al nombre Dockerfile! Es muy importante que el archivo se llame exactamente Dockerfile, sin ninguna extensión como .txt. Si usas el Bloc de notas, al guardar, asegúrate de cambiar el "Tipo" a "Todos los archivos (*.*)" para evitar que Windows le añada una extensión automáticamente.

A continuación, este es el contenido que debes poner en cada archivo.

Vamos a crear una imagen para una aplicación web simple con Python y Flask. Primero, la aplicación (app.py):

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "¡Hola, mundo desde un contenedor Docker!"

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

Y sus dependencias (requirements.txt):

Flask>=2.3

Ahora, el Dockerfile que empaqueta esta aplicación:

# 1. Especificar la imagen base
FROM python:3.9-slim

# 2. Establecer el directorio de trabajo
WORKDIR /app

# 3. Copiar e instalar dependencias
COPY requirements.txt .
RUN pip install -r requirements.txt

# 4. Copiar el código de la aplicación
COPY . .

# 5. Exponer el puerto que usa la aplicación
EXPOSE 5000

# 6. Definir el comando para ejecutar la aplicación
CMD ["flask", "run", "--host=0.0.0.0"]

Instrucciones principales del Dockerfile

  • FROM: Siempre es la primera instrucción. Define la imagen base sobre la que se construirá la nuestra. Es buena idea usar imágenes oficiales y específicas (ej. `python:3.9-slim` en lugar de solo `python`).
  • WORKDIR: Establece el directorio de trabajo para las instrucciones siguientes (`RUN`, `CMD`, `COPY`, etc.). Si no existe, lo crea.
  • COPY: Copia archivos o directorios desde tu máquina local (el contexto de build) al sistema de archivos de la imagen. COPY <origen> <destino>.
  • ADD: Similar a `COPY`, pero con funcionalidades extra, como descomprimir archivos `tar` automáticamente o usar URLs como origen. En general, se prefiere `COPY` por ser más explícito.
  • RUN: Ejecuta un comando en una nueva capa. Se usa para instalar paquetes (`RUN apt-get update`), dependencias (`RUN pip install`) o compilar código.
  • EXPOSE: Informa a Docker que el contenedor escuchará en un puerto de red específico en tiempo de ejecución. No publica el puerto, solo sirve como documentación.
  • CMD: Proporciona el comando por defecto que se ejecutará cuando se inicie un contenedor a partir de la imagen. Solo puede haber un `CMD` en un Dockerfile. Si el usuario especifica un comando al hacer `docker run`, el `CMD` se ignora.
  • ENTRYPOINT: También define un comando a ejecutar, pero está pensado para que la imagen se comporte como un ejecutable. A diferencia de `CMD`, no se sobrescribe fácilmente. Lo que se pasa en el `docker run` se añade como argumento al `ENTRYPOINT`.

Construcción y ejecución de la imagen

Construcción: `docker build`

Con los tres archivos (`app.py`, `requirements.txt`, `Dockerfile`) en el mismo directorio, ejecuta el siguiente comando:

docker build -t mi-app-flask:1.0 .
  • -t mi-app-flask:1.0: Etiqueta (tag) la imagen con un nombre y una versión. Es una práctica fundamental.
  • .: El punto final indica el contexto de la construcción (build context), es decir, el directorio que contiene el Dockerfile y los archivos de la aplicación.

Verás cómo Docker ejecuta cada instrucción del Dockerfile, creando una capa para cada una.

Ejecución de la imagen construida

Una vez construida, la ejecutamos como cualquier otra imagen:

docker run -d -p 5000:5000 --name mi-app mi-app-flask:1.0

Si ahora vas a http://localhost:5000 en tu navegador, verás el mensaje de la aplicación Flask. ¡Tu aplicación ahora está contenedorizada!

Salida del comando docker build para la aplicación Flask

Ejemplo 2: Una API simple con FastAPI

Cambiemos de nuevo el ejemplo para usar FastAPI, un framework web moderno y de alto rendimiento para crear APIs con Python. El objetivo será crear un endpoint / que devuelva un objeto JSON.

Una vez más, modifica el contenido de tus archivos de proyecto.

1. Modifica app.py:

Este es el código para una API mínima con FastAPI.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"message": "Hola mundo"}

2. Modifica requirements.txt:

FastAPI necesita un servidor ASGI como Uvicorn para ejecutarse.

fastapi==0.85.0
uvicorn[standard]==0.18.3

3. Modifica tu Dockerfile:

El Dockerfile debe ser adaptado para instalar las nuevas dependencias y para usar uvicorn para lanzar la aplicación.

# 1. Imagen base
FROM python:3.9-slim

# 2. Directorio de trabajo
WORKDIR /app

# 3. Instalar dependencias
COPY requirements.txt .
RUN pip install -r requirements.txt

# 4. Copiar aplicación
COPY app.py .

# 5. Exponer el puerto por defecto de Uvicorn
EXPOSE 8000

# 6. Comando para iniciar el servidor con Uvicorn
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Construcción y Ejecución

Construimos la imagen con una etiqueta específica para FastAPI.

# Construir la imagen de la API
docker build -t mi-app-fastapi:1.0 .

Al ejecutar, mapeamos un puerto de nuestra máquina (p. ej. 8000) al puerto del contenedor (8000).

# Ejecutar el contenedor mapeando el puerto 8000 al 8000
docker run --rm -p 8000:8000 mi-app-fastapi:1.0

Si ahora visitas http://localhost:8000 en tu navegador, verás la respuesta JSON: {"message":"Hola mundo"}.

Respuesta JSON de la API con FastAPI
Respuesta JSON en el navegador al acceder al contenedor con FastAPI.

Buenas prácticas en Dockerfiles

  • Orden lógico y caché de capas: Ordena las instrucciones de la menos a la más cambiante. En nuestro ejemplo, copiamos e instalamos `requirements.txt` antes de copiar el resto del código. Así, si solo cambiamos `app.py`, Docker reutiliza la capa de las dependencias ya instaladas, haciendo el `build` mucho más rápido.
  • Capas mínimas: Cada `RUN` crea una capa. Agrupa comandos relacionados con `&&` para reducir el número de capas y el tamaño final de la imagen.
    RUN apt-get update && apt-get install -y curl
  • Usa imágenes base pequeñas: Empieza con imágenes como `alpine` o `slim` siempre que sea posible. Reducen drásticamente el tamaño de tu imagen final y la superficie de ataque.
  • Multi-stage builds: Para lenguajes compilados (Go, Java, C#) o aplicaciones que requieren un paso de `build` (React, Angular), se usa un `build` multi-etapa. Una primera etapa compila el código o empaqueta los assets, y una segunda etapa, mucho más ligera, solo copia el artefacto final, descartando todas las dependencias de compilación.