DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on

LLPY-12: Docker y Containerización - De Desarrollo a Producción

🎯 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

  1. 🔄 Reproducibilidad: Mismo comportamiento en dev, staging, prod
  2. 📦 Portabilidad: Ejecuta en cualquier servidor Linux
  3. 🔒 Aislamiento: Dependencias no interfieren con host
  4. ⚡ Velocidad: Deploy en segundos, no horas
  5. 📊 Versionado: Cada deploy es traceable
  6. ↔️ 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
Enter fullscreen mode Exit fullscreen mode

Con Docker:

docker run -p 8000:8000 --env-file .env username/lus-laboris-api:latest

TOTAL: 2 minutos
Enter fullscreen mode Exit fullscreen mode

💡 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
Enter fullscreen mode Exit fullscreen mode

🏗️ 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 []
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🐳 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Uso:

cd src/lus_laboris_api
chmod +x docker_build_push.sh
./docker_build_push.sh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

📊 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
Enter fullscreen mode Exit fullscreen mode

2. UV locked dependencies:

COPY pyproject.toml uv.lock ./
RUN uv sync --locked  # ⬅️ No descarga extras, exactas versiones
Enter fullscreen mode Exit fullscreen mode

3. Non-root user:

RUN useradd apiuser
USER apiuser  # ⬅️ Seguridad + compatibilidad Cloud Run
Enter fullscreen mode Exit fullscreen mode

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/*
Enter fullscreen mode Exit fullscreen mode

🚀 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
Enter fullscreen mode Exit fullscreen mode

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

Documentación Técnica

Recursos Externos


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)