🎯 El Desafío de la Portabilidad y Reproducibilidad
Imagina este escenario familiar:
- ✅ Tu código funciona perfecto en tu laptop
- ❌ Falla en el servidor de staging
- ❌ Falla diferente en producción
- ❌ Nuevo desarrollador tarda 2 días en setup
El problema: "Works on my machine" 🤷
Requisitos de Deployment Moderno
- 🔄 Reproducibilidad: Mismo comportamiento en dev, staging, prod
- 📦 Portabilidad: Ejecuta en cualquier servidor Linux
- 🔒 Aislamiento: Dependencias no interfieren con host
- ⚡ Velocidad: Deploy en segundos, no horas
- 📊 Versionado: Cada deploy es traceable
- ↔️ Consistencia: Python 3.13, UV, dependencias exactas
Opciones para Deployment
Método | Pros | Contras | Reproducibilidad |
---|---|---|---|
Manual (pip install) | Simple | Dependencias conflictivas | ❌ Ninguna |
Virtual environments | Aislamiento Python | No aísla sistema | ⚠️ Parcial |
Conda environments | Multi-lenguaje | Pesado, lento | ⚠️ Media |
VM images | Aislamiento total | Pesado (GBs), lento | ✅ Alta |
Docker containers | Ligero, rápido, portable | Learning curve | ✅✅ Muy Alta |
Nuestra elección: Docker
📊 La Magnitud del Problema
Sin Containerización
Setup en servidor nuevo:
# 1. Instalar Python 3.13 (20 minutos)
sudo apt update
sudo apt install python3.13
# Error: "python3.13 not found in repository"
# Compilar desde source... (60 minutos)
# 2. Instalar UV (5 minutos)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 3. Clonar repo y instalar deps (10 minutos)
git clone ...
cd project
uv sync
# Error: "Conflicto con paquete del sistema"
# 4. Configurar env vars (5 minutos)
# Copiar .env, ajustar paths...
# 5. Debugging de problemas (120 minutos)
# "¿Por qué no encuentra librerías?"
# "¿OpenSSL version incompatible?"
TOTAL: 3-4 horas + frustración
Con Docker:
docker run -p 8000:8000 --env-file .env username/lus-laboris-api:latest
TOTAL: 2 minutos
💡 La Solución: Docker Containers
¿Qué es Docker?
Docker es una plataforma de containerización que permite:
- 📦 Empaquetar aplicación + dependencias + runtime
- 🚀 Ejecutar de forma aislada en cualquier servidor
- 🔄 Distribuir via registries (Docker Hub, GCR)
- 📊 Versionar cada build con tags
- ⚡ Iniciar en segundos (vs minutos de VMs)
Container vs VM
┌─────────────────────────────────────────────┐
│ Virtual Machine Architecture │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ App 1 │ │ App 2 │ │ App 3 │ │
│ ├─────────┤ ├─────────┤ ├─────────┤ │
│ │ Python │ │ Node │ │ Java │ │
│ ├─────────┤ ├─────────┤ ├─────────┤ │
│ │Guest OS │ │Guest OS │ │Guest OS │ │ Heavy!
│ │ (1GB+) │ │ (1GB+) │ │ (1GB+) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ─────────────────────────────────── │
│ Hypervisor (VMware, KVM) │
│ ─────────────────────────────────── │
│ Host OS (Linux) │
│ ─────────────────────────────────── │
│ Hardware │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Docker Container Architecture │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ App 1 │ │ App 2 │ │ App 3 │ │
│ │ +Python │ │ +Node │ │ +Java │ │
│ │ +Libs │ │ +Libs │ │ +Libs │ │ Light!
│ │ (100MB) │ │ (80MB) │ │ (150MB) │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ─────────────────────────────────── │
│ Docker Engine (containerd) │
│ ─────────────────────────────────── │
│ Host OS (Linux) │
│ ─────────────────────────────────── │
│ Hardware │
└─────────────────────────────────────────────┘
Ventajas de Containers:
✅ 10-100x más ligero
✅ Inicia en segundos (vs minutos)
✅ Menos overhead de CPU/RAM
✅ Comparte kernel del host
🏗️ Dockerfiles del Proyecto
1. Dockerfile para Procesamiento (Batch)
Archivo src/processing/Dockerfile
:
# Base image: Python 3.13 slim (Debian Bookworm)
FROM python:3.13.5-slim-bookworm
# Copy UV from official image
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Create non-root user
RUN useradd --create-home --shell /bin/bash appuser
# Set working directory
WORKDIR /app
# Environment variables
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Copy dependency files (leverage Docker cache)
COPY --chown=appuser:appuser pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --locked
# Copy source code
COPY --chown=appuser:appuser extract_law_text.py ./
# Switch to non-root user
USER appuser
# Entrypoint
ENTRYPOINT ["uv", "run", "extract_law_text.py"]
# CMD empty to allow flexible args
CMD []
Características clave:
- ✅ Multi-stage: Copia UV desde imagen oficial (no instalación)
- ✅ Layer caching: Dependencies primero, código después
- ✅ Non-root user: Seguridad (usuario
appuser
) - ✅ UV locked: Dependencias exactas con
uv.lock
- ✅ Flexible args: CMD vacío permite pasar argumentos
Build:
cd src/processing
docker build -t username/lus-laboris-processing:latest .
Run:
# Con argumentos personalizados
docker run username/lus-laboris-processing:latest --input data.pdf --output output.json
# Con volúmenes para datos
docker run -v $(pwd)/data:/app/data username/lus-laboris-processing:latest
2. Dockerfile para API (FastAPI)
Archivo src/lus_laboris_api/Dockerfile
:
# Base image: Python 3.13 slim
FROM python:3.13.5-slim-bookworm
# Copy UV from official image
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Install build dependencies and create user
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --shell /bin/bash apiuser
# Set working directory
WORKDIR /app
# Environment variables
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Copy dependency files (caching layer)
COPY --chown=apiuser:apiuser pyproject.toml uv.lock ./
# Install dependencies
RUN uv sync --locked
# Copy source code
COPY --chown=apiuser:apiuser api ./api
# Switch to non-root user
USER apiuser
# Expose port
EXPOSE 8000
# Entrypoint
ENTRYPOINT ["uv", "run", "uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"]
Diferencias con Dockerfile de procesamiento:
- ✅ Build tools: Incluye
build-essential
para compilar deps - ✅ Port exposure:
EXPOSE 8000
para Cloud Run - ✅ Uvicorn entrypoint: Inicia servidor ASGI
- ✅ Cleanup: Elimina apt cache para reducir tamaño
Build:
cd src/lus_laboris_api
docker build -t username/lus-laboris-api:latest .
Run:
# Desarrollo local
docker run -p 8000:8000 --env-file .env username/lus-laboris-api:latest
# Acceder a API
curl http://localhost:8000/api/health
🐳 Docker Compose para Desarrollo
Stack Completo: API + Qdrant + Phoenix
Archivo src/lus_laboris_api/docker-compose.yml
:
services:
# Qdrant vector database
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
ports:
- "6333:6333" # HTTP API
- "6334:6334" # gRPC API
volumes:
- qdrant_storage:/qdrant/storage
environment:
- QDRANT__SERVICE__HTTP_PORT=6333
- QDRANT__SERVICE__GRPC_PORT=6334
- QDRANT__LOG_LEVEL=INFO
restart: always
networks:
- api-network
# Phoenix observability
phoenix:
image: arizephoenix/phoenix:latest
container_name: phoenix
ports:
- "6006:6006" # HTTP UI
- "4317:4317" # gRPC collector
environment:
- PHOENIX_PORT=6006
- PHOENIX_GRPC_PORT=4317
restart: unless-stopped
networks:
- api-network
# Lus Laboris API
api:
build: .
container_name: lus-laboris-api
ports:
- "8000:8000"
env_file:
- ../../.env
environment:
- API_QDRANT_URL=http://qdrant:6333
- API_PHOENIX_ENDPOINT=http://phoenix:6006
- API_PHOENIX_GRPC_ENDPOINT=phoenix:4317
volumes:
- ../../keys/public_key.pem:/app/api/keys/public_key.pem:ro
depends_on:
- qdrant
- phoenix
restart: unless-stopped
networks:
- api-network
volumes:
qdrant_storage:
driver: local
networks:
api-network:
driver: bridge
Uso:
# Iniciar todo el stack
cd src/lus_laboris_api
docker-compose up -d
# Ver logs
docker-compose logs -f
# Solo logs de API
docker-compose logs -f api
# Verificar servicios
curl http://localhost:6333/collections # Qdrant
curl http://localhost:6006 # Phoenix UI
curl http://localhost:8000/api/health # API
# Detener stack
docker-compose down
# Detener y eliminar volúmenes (⚠️ borra datos)
docker-compose down -v
Ventajas:
- ✅ Single command: Todo el stack con
docker-compose up
- ✅ Networking: Containers se comunican por nombre (api → qdrant)
- ✅ Persistencia: Volúmenes para datos de Qdrant
- ✅ Restart policies: Auto-restart si container crash
📦 Optimización de Imágenes Docker
1. Multi-Stage Builds (Si Fuera Necesario)
# Stage 1: Builder (con todas las build tools)
FROM python:3.13 AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
# Instalar deps incluyendo build tools
RUN uv sync --locked
# Stage 2: Runtime (solo lo necesario)
FROM python:3.13-slim
COPY --from=builder /app/.venv /app/.venv
WORKDIR /app
COPY api ./api
ENV PATH="/app/.venv/bin:$PATH"
CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0"]
Beneficio: Imagen final no incluye build tools → 40-50% más pequeña
2. .dockerignore
Archivo src/lus_laboris_api/.dockerignore
:
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
.venv/
# Environment
.env
.env.local
# IDE
.vscode/
.idea/
*.swp
# Git
.git/
.gitignore
# Testing
.pytest_cache/
.coverage
htmlcov/
# Documentation
*.md
docs/
# CI/CD
.github/
# Large files
*.pdf
*.zip
data/
Beneficio: Build más rápido, imagen más pequeña (no copia archivos innecesarios)
3. Layer Caching Strategy
# ❌ MAL: Dependencies después de código
COPY api ./api
COPY pyproject.toml uv.lock ./
RUN uv sync
# Problema: Cambio en api/ → re-install de TODAS las deps
# ✅ BIEN: Dependencies primero
COPY pyproject.toml uv.lock ./
RUN uv sync --locked
COPY api ./api
# Beneficio: Cambio en api/ → solo re-copy código (cache de deps)
Mejora: Build de 5 minutos → 30 segundos en iteraciones
🚀 Build y Publicación en Docker Hub
1. Script Automatizado
Archivo src/lus_laboris_api/docker_build_push.sh
:
#!/bin/bash
set -e
# Load variables from .env
if [[ -f "../../.env" ]]; then
set -o allexport
source ../../.env
set +o allexport
fi
# Validate required variables
if [[ -z "$DOCKER_HUB_USERNAME" || -z "$DOCKER_HUB_PASSWORD" || -z "$DOCKER_IMAGE_NAME_RAG_API" ]]; then
echo "❌ ERROR: Variables requeridas no definidas"
exit 1
fi
# Login to Docker Hub
echo "$DOCKER_HUB_PASSWORD" | docker login --username "$DOCKER_HUB_USERNAME" --password-stdin
# Define tags
DATE_TAG=$(date +%Y%m%d)
LATEST_TAG="latest"
# Build image
echo "🏗️ Building image..."
docker build -t "$DOCKER_HUB_USERNAME/$DOCKER_IMAGE_NAME_RAG_API:$DATE_TAG" .
# Tag as latest
docker tag "$DOCKER_HUB_USERNAME/$DOCKER_IMAGE_NAME_RAG_API:$DATE_TAG" \
"$DOCKER_HUB_USERNAME/$DOCKER_IMAGE_NAME_RAG_API:$LATEST_TAG"
# Push both tags
echo "📤 Pushing to Docker Hub..."
docker push "$DOCKER_HUB_USERNAME/$DOCKER_IMAGE_NAME_RAG_API:$DATE_TAG"
docker push "$DOCKER_HUB_USERNAME/$DOCKER_IMAGE_NAME_RAG_API:$LATEST_TAG"
echo ""
echo "✅ Imágenes subidas a Docker Hub:"
echo " $DOCKER_HUB_USERNAME/$DOCKER_IMAGE_NAME_RAG_API:$DATE_TAG"
echo " $DOCKER_HUB_USERNAME/$DOCKER_IMAGE_NAME_RAG_API:$LATEST_TAG"
Uso:
cd src/lus_laboris_api
chmod +x docker_build_push.sh
./docker_build_push.sh
Output:
🏗️ Building image...
[+] Building 45.3s (12/12) FINISHED
=> [1/6] FROM python:3.13.5-slim-bookworm
=> [2/6] COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
=> [3/6] RUN useradd --create-home apiuser
=> [4/6] COPY pyproject.toml uv.lock ./
=> [5/6] RUN uv sync --locked
=> [6/6] COPY api ./api
📤 Pushing to Docker Hub...
The push refers to repository [docker.io/username/lus-laboris-api]
20241016: digest: sha256:abc123... size: 2415
latest: digest: sha256:abc123... size: 2415
✅ Imágenes subidas a Docker Hub:
username/lus-laboris-api:20241016
username/lus-laboris-api:latest
2. GitHub Actions para Build Automático
Archivo .github/workflows/docker-api-build-publish.yml
:
name: Build & Publish Docker Image (API)
on:
workflow_dispatch: # Manual trigger
push:
paths:
- 'src/lus_laboris_api/Dockerfile'
- 'src/lus_laboris_api/pyproject.toml'
- 'src/lus_laboris_api/uv.lock'
- 'src/lus_laboris_api/api/**'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Get date tag
id: date_tag
run: echo "tag=$(date +'%Y%m%d')" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./src/lus_laboris_api
push: true
tags: |
${{ secrets.DOCKER_HUB_USERNAME }}/lus-laboris-api:latest
${{ secrets.DOCKER_HUB_USERNAME }}/lus-laboris-api:${{ steps.date_tag.outputs.tag }}
cache-from: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/lus-laboris-api:latest
cache-to: type=inline
Ventajas:
- ✅ Auto-build: Push a GitHub → build automático
- ✅ Buildx: Multi-platform builds (amd64, arm64)
- ✅ Cache: Reutiliza layers de builds anteriores
- ✅ Dual tags:
latest
+ fecha para rollback
🎯 Casos de Uso Reales
Para Desarrollo Local:
"Quiero desarrollo consistente sin instalar Python/UV"
Solución:
# Docker Compose levanta TODO
cd src/lus_laboris_api
docker-compose up -d
# API en http://localhost:8000
# Qdrant en http://localhost:6333
# Phoenix en http://localhost:6006
# Código cambia → rebuild solo API:
docker-compose up -d --build api
Para Onboarding de Nuevos Desarrolladores:
"Nuevo dev necesita setup en 5 minutos"
Solución:
# Paso 1: Clonar repo
git clone https://github.com/user/lus-laboris-py.git
cd lus-laboris-py
# Paso 2: Copiar .env
cp .env.example .env
# Editar con tus API keys
# Paso 3: Docker Compose
cd src/lus_laboris_api
docker-compose up -d
# Paso 4: Verificar
curl http://localhost:8000/docs
# ✅ DONE en 5 minutos
Para Deployment en Cloud Run:
"Necesito deployar en GCP"
Solución:
# Build y push
./docker_build_push.sh
# Deploy con gcloud
gcloud run deploy lus-laboris-api \
--image username/lus-laboris-api:20241016 \
--platform managed \
--region us-central1
# ✅ API en producción en 3 minutos
Para Testing en CI/CD:
"Quiero correr tests en ambiente idéntico a producción"
Solución:
# .github/workflows/test.yml
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build test image
run: docker build -t api:test .
- name: Run tests in container
run: docker run api:test pytest tests/
📊 Optimización de Imágenes
Tamaños de Imagen
Imagen | Base | Con deps | Final | Optimización |
---|---|---|---|---|
API (sin optimizar) | python:3.13 (1GB) | +500MB | 1.5GB | ❌ |
API (slim) | python:3.13-slim (150MB) | +180MB | 330MB | ✅ 78% reducción |
API (alpine) | python:3.13-alpine (50MB) | +200MB | 250MB | ✅✅ 83% reducción |
Processing | python:3.13-slim | +120MB | 270MB | ✅ |
Nuestra elección: python:3.13-slim
- ✅ Balance tamaño/compatibilidad
- ✅ Debian-based (mayor compatibilidad de deps)
- ✅ 78% más pequeño que
python:3.13
Técnicas de Optimización
1. Cleanup de apt cache:
RUN apt-get update && \
apt-get install -y build-essential && \
rm -rf /var/lib/apt/lists/* # ⬅️ Elimina 50-100MB
2. UV locked dependencies:
COPY pyproject.toml uv.lock ./
RUN uv sync --locked # ⬅️ No descarga extras, exactas versiones
3. Non-root user:
RUN useradd apiuser
USER apiuser # ⬅️ Seguridad + compatibilidad Cloud Run
4. Minimize layers:
# ❌ MAL: 3 layers
RUN apt-get update
RUN apt-get install -y build-essential
RUN rm -rf /var/lib/apt/lists/*
# ✅ BIEN: 1 layer
RUN apt-get update && \
apt-get install -y build-essential && \
rm -rf /var/lib/apt/lists/*
🚀 El Impacto Transformador
Antes de Docker:
- ⏱️ Setup time: 2-4 horas por servidor
- 🐛 "Works on my machine": Constante debugging de entorno
- 📦 Dependency hell: Conflictos con sistema
- 🔄 Inconsistencia: Dev ≠ Staging ≠ Prod
- 💰 Desperdicio: Múltiples VMs para aislamiento
Después de Docker:
- ⚡ Setup time: 2-5 minutos con Docker Compose
- ✅ Guaranteed consistency: Dev = Staging = Prod
- 📦 Dependency isolation: Zero conflictos
- 🔄 Perfect parity: Mismo container en todos lados
- 💰 Efficiency: Múltiples containers en 1 servidor
Métricas de Mejora:
Aspecto | Sin Docker | Con Docker | Mejora |
---|---|---|---|
Setup time | 2-4 horas | 2-5 minutos | -95% |
Environment issues | Frecuentes | Raros | -90% |
Deploy time | 30-60 min | 2-3 min | -95% |
Rollback time | 30 min | 30 seg | -97% |
Onboarding (new dev) | 1-2 días | 10 minutos | -99% |
💡 Lecciones Aprendidas
1. Slim > Full > Alpine
Para Python, python:3.13-slim
es el sweet spot. Alpine tiene problemas con deps compiladas.
2. Copy Dependencies First
Layer caching es crítico. Dependencies cambian poco, código cambia mucho.
3. Non-Root User es Requerido
Cloud Run y muchos orchestrators requieren non-root. Hazlo desde el principio.
4. UV en Docker es Magia
UV es 10-100x más rápido que pip. Copia desde imagen oficial = zero install time.
5. .dockerignore es Crítico
Sin .dockerignore, builds lentos y imágenes grandes (copia .venv
, data/
, etc.)
6. Health Checks en Dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/api/health || exit 1
Permite a orchestrators (Docker Compose, Kubernetes) detectar containers unhealthy.
🎯 El Propósito Más Grande
Docker no es solo containerización - es portabilidad universal. Al empaquetar:
- 📦 Runtime: Python 3.13
- 📚 Dependencies: UV + todas las libs
- ⚙️ Configuration: Environment variables
- 🔧 Code: Aplicación completa
En un artifact inmutable, logramos:
- ✅ Build once, run anywhere: Dev, staging, prod, cloud, on-prem
- ✅ Versionado exacto: Cada imagen es traceable (tag + digest)
- ✅ Rollback instant: Cambiar tag en deploy = rollback
- ✅ Scaling horizontal: Múltiples containers de la misma imagen
- ✅ Testing aislado: Tests en container = env idéntico a prod
- ✅ Zero config drift: Configuración versionada en Dockerfile
Estamos eliminando la categoría completa de errores "funciona en mi máquina" y acelerando deployment de horas a minutos.
🔗 Recursos y Enlaces
Repositorio del Proyecto
- GitHub: lus-laboris-py
Documentación Técnica
-
API Dockerfile:
src/lus_laboris_api/Dockerfile
-
Processing Dockerfile:
src/processing/Dockerfile
-
Docker Compose:
src/lus_laboris_api/docker-compose.yml
-
Build Script:
src/lus_laboris_api/docker_build_push.sh
Recursos Externos
- Docker Docs: docs.docker.com
- Docker Hub: hub.docker.com
- Dockerfile Best Practices: docs.docker.com/develop/dev-best-practices
- Docker Compose Docs: docs.docker.com/compose
Próximo Post: LLPY-13 - CI/CD con GitHub Actions
En el siguiente post exploraremos el sistema completo de CI/CD con 7 workflows automatizados: quality checks, Docker builds, Terraform apply, y deployments a GCP.
Top comments (0)