Deploy de Agentes — Módulo 5: contenerización — Docker para tu agente
Quinto módulo del Curso de Deploy de Agentes: por qué contenedores matan el 'en mi máquina funciona'. Qué es una imagen, un contenedor, un Dockerfile y un registry. Un Dockerfile real y correcto para un agente Python, multi-stage build, .dockerignore y los comandos docker build y docker run.
☰ Índice del contenido
- La tesis del módulo
- El problema real: el entorno es parte del código
- Un Dockerfile real para un agente Python
- .dockerignore: lo que NO entra
- Multi-stage build: cuando necesitas compilar
- Construir y correr: los dos comandos
- Buenas prácticas que valen oro
- Errores comunes
- La frase para llevarse
- Qué viene
- Material relacionado
En el módulo anterior separamos la configuración del código: los secretos salen por variables de entorno y nunca viven dentro del repositorio. Buenísimo — pero queda un cabo suelto que arruina más deploys que cualquier bug: tu agente corre perfecto en tu notebook y explota en el servidor. Otra versión de Python, una librería del sistema que faltaba, una variable que estaba solo en tu máquina. El clásico “en mi máquina funciona”. Este módulo mata esa frase de raíz con contenedores.
La tesis del módulo
Un contenedor empaqueta tu agente con todo lo que necesita para correr — Python, librerías, código — en una caja idéntica en tu máquina y en producción. Si corre en tu contenedor, corre en el servidor. Docker deja de hacer del deploy una lotería.
El problema real: el entorno es parte del código
Tu agente no es solo el .py. Es tu .py más la versión exacta de Python, más cada librería con su versión precisa, más las dependencias del sistema operativo. Cuando entregas solo el código y esperas que el servidor “tenga lo demás”, estás apostando. Un contenedor entrega el paquete completo, congelado.
Cuatro conceptos que se confunden todo el tiempo, glosados en orden:
- Imagen — el molde congelado. Una plantilla de solo lectura que contiene tu app y su entorno completo. Se construye una vez y no cambia. Piensa en el instalador o el molde de una pieza.
- Contenedor — el molde en movimiento. Una instancia viva de una imagen, corriendo como un proceso aislado. De una imagen puedes levantar uno o cincuenta contenedores idénticos.
- Dockerfile — la receta. Un archivo de texto con los pasos para construir la imagen: de qué base partir, qué copiar, qué instalar, qué comando ejecutar al arrancar.
- Registry — la bodega. Un repositorio donde subes y bajas imágenes (Docker Hub, GitHub Container Registry, Artifact Registry de Google). Construyes la imagen una vez, la subes al registry y el servidor la baja tal cual.
Dockerfile ──► docker build ──► imagen
(la receta) (el molde)
│
docker run│
▼
┌──────────┬──────────┬──────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ cont. │ │ cont. │ │ cont. │ │ cont. │
│ #1 │ │ #2 │ │ #3 │ │ #4 │
└───────┘ └───────┘ └───────┘ └───────┘
una imagen, muchos contenedores iguales
La imagen es el sustantivo; el contenedor es el verbo. Una imagen no “corre” — es un archivo. El contenedor es esa imagen ejecutándose.
Un Dockerfile real para un agente Python
Aquí no hay adornos: este es un Dockerfile correcto y listo para producción de un agente Python. Cada línea comentada.
# Imagen base: Python 3.12 en su variante "slim" (Debian mínimo,
# sin herramientas de compilación que no usas en runtime).
FROM python:3.12-slim
# Directorio de trabajo dentro del contenedor. Todo lo que sigue
# ocurre relativo a /app.
WORKDIR /app
# Copiamos SOLO requirements.txt primero (no todo el proyecto).
# Esto es clave para la caché de capas: si tu código cambia pero
# tus dependencias no, Docker reutiliza la capa de pip sin reinstalar.
COPY requirements.txt .
# Instalamos las dependencias. --no-cache-dir evita que pip guarde
# su caché dentro de la imagen (la imagen queda más liviana).
RUN pip install --no-cache-dir -r requirements.txt
# Ahora sí copiamos el código de la aplicación.
COPY src/ ./src
# Comando que se ejecuta al arrancar el contenedor: corre tu agente
# como módulo. Forma "exec" (lista JSON), no string, para que las
# señales de apagado lleguen bien al proceso.
CMD ["python", "-m", "src.main"]
El orden de esas líneas no es estético: es rendimiento. Docker construye la imagen en capas, una por instrucción, y cachea cada una. Como requirements.txt cambia mucho menos que tu código, copiarlo e instalar antes de copiar src/ hace que un cambio de código no vuelva a instalar todas las librerías. Un build que tardaba tres minutos pasa a tardar tres segundos.
Ojo con los secretos. Nada de
ENV OPENAI_API_KEY=sk-...ni de copiar tu.envdentro de la imagen. Una imagen es pública para quien la baje del registry, y sus capas quedan en el historial aunque después las “borres”. Los secretos entran en tiempo de ejecución, por entorno, tal como vimos en el módulo 4. La imagen es el molde; el secreto es agua que echas recién al usar el molde.
.dockerignore: lo que NO entra
Igual que .gitignore, un archivo .dockerignore en la raíz evita que basura y secretos se cuelen en la imagen cuando haces COPY. Sin él, un COPY . . distraído se lleva tu .env, tu carpeta .git de 200 MB y el venv local.
# Secretos y entorno local: JAMÁS dentro de la imagen
.env
.env.*
# Ruido que infla la imagen y rompe la caché
.git
__pycache__/
*.pyc
venv/
.venv/
.pytest_cache/
tests/
Multi-stage build: cuando necesitas compilar
Algunas dependencias de Python (las que traen extensiones en C) necesitan un compilador para instalarse, pero ese compilador no hace falta al correr. Un multi-stage build usa una etapa “pesada” para construir y copia solo el resultado a una etapa final liviana. La imagen final no carga con el compilador.
# --- Etapa 1: builder (tiene herramientas de compilación) ---
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
# Instala las dependencias en una carpeta aislada que podamos copiar
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# --- Etapa 2: runtime (limpia, solo lo necesario para correr) ---
FROM python:3.12-slim
WORKDIR /app
# Copiamos SOLO las dependencias ya construidas desde la etapa builder
COPY --from=builder /install /usr/local
COPY src/ ./src
CMD ["python", "-m", "src.main"]
Para un agente simple el Dockerfile de una sola etapa alcanza y sobra. El multi-stage se paga solo cuando tus dependencias compilan cosas o cuando el tamaño de la imagen empieza a doler.
Construir y correr: los dos comandos
# Construye la imagen y le pone un nombre (tag) con -t.
# El punto final es el "contexto": la carpeta que Docker envía al build.
docker build -t mi-agente:1.0 .
# Corre un contenedor a partir de esa imagen.
# --rm : borra el contenedor al terminar (no deja basura)
# -e : inyecta un secreto por entorno, en runtime, no en la imagen
# --env-file: alternativa para cargar un archivo de variables completo
docker run --rm -e OPENAI_API_KEY=$OPENAI_API_KEY mi-agente:1.0
El -e OPENAI_API_KEY=$OPENAI_API_KEY es exactamente el punto del módulo 4 aterrizado: el secreto vive en tu entorno (o en el gestor de secretos del servidor) y se inyecta al arrancar el contenedor. La imagen sigue siendo genérica y compartible.
Buenas prácticas que valen oro
| Práctica | Por qué importa |
|---|---|
Base -slim (o alpine con cuidado) | Imagen liviana: baja más rápido del registry, menos superficie de ataque |
Copiar requirements.txt antes que el código | La caché de capas no reinstala dependencias si solo cambió el código |
--no-cache-dir en pip | No deja la caché de pip dentro de la imagen final |
.dockerignore siempre | Evita que .env, .git y venv entren en la imagen |
Fijar versión de la base (python:3.12-slim) | python:latest cambia bajo tus pies y rompe builds reproducibles |
Secretos por -e / --env-file, nunca en ENV | La imagen es compartible; el secreto no debe viajar dentro |
Tag explícito (mi-agente:1.0), no latest | Sabes exactamente qué versión está corriendo en producción |
Advertencia sobre
latest. Tanto en la imagen base como en tu propio tag,latestes un puntero móvil: hoy apunta a una cosa, mañana a otra. Para reproducibilidad — el corazón de este curso — fija versiones.latesten producción es la puerta trasera del “en mi máquina funciona” volviendo por la ventana.
Errores comunes
- Copiar todo el proyecto de una (
COPY . .) sin.dockerignore. Te llevas el.env, el.gity elvenvdentro de la imagen. Lento, pesado e inseguro. - Meter secretos con
ENVo copiando el.env. Quedan grabados en las capas de la imagen para siempre, aunque los borres después. Los secretos van en runtime. - Poner
COPY src/antes de instalar dependencias. Rompe la caché de capas: cada cambio de código reinstala todas las librerías desde cero. - Usar
python:3.12(completa) cuando-slimalcanza. Cientos de MB extra que nunca usas, solo para bajar y subir más lento. - Escribir el
CMDcomo string en vez de lista JSON.CMD python -m src.maincorre dentro de un shell y las señales de apagado no llegan limpias al proceso; usa la forma["python", "-m", "src.main"]. - No fijar la versión de la base.
FROM python:latestconstruye distinto según el día. Reproducibilidad cero.
La frase para llevarse
“La imagen es el molde; el contenedor, la pieza; el secreto, el agua que echas recién al usarlo. Nunca fundas el agua dentro del molde.”
Qué viene
Módulo 6: testing de agentes I — unit tests y mockear el LLM. Ya tenemos el agente empaquetado y reproducible; ahora hay que asegurarnos de que funciona antes de construir la imagen. Empezamos por la parte determinista — tools, parsing, routing — y aprendemos a reemplazar el LLM por un doble controlado para que los tests sean rápidos, gratis y confiables.
Material relacionado
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.