DEV Community

Cover image for LLPY-12: Docker y Containerización - De Desarrollo a Producción
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 la API

El archivo src/lus_laboris_api/docker-compose.yml del proyecto es simple y solo contiene la API:

services:
  legal-rag-api:
    image: rj24/legal-rag-api:latest
    container_name: legal-rag-api
    ports:
      - 8000:8000
    environment:
      API_ENV_FILE_PATH: /app/.env
    volumes:
      - ../../keys/public_key.pem:/app/api/keys/public_key.pem:ro
      - ../../.env:/app/.env:ro
    restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

Características:

  • Solo API: Qdrant y Phoenix se levantan con services/manage_services.sh
  • Imagen pre-built: Usa imagen de Docker Hub (no build local)
  • Monta secrets: JWT key y .env desde host
  • Restart policy: Se reinicia si crash

Uso:

# PASO 1: Levantar servicios externos
cd services
./manage_services.sh  # Opción 's' → 'Todos los servicios'

# PASO 2: Levantar API
cd ../src/lus_laboris_api
docker-compose up -d

# Verificar servicios
curl http://localhost:6333/collections  # Qdrant (del paso 1)
curl http://localhost:6006              # Phoenix (del paso 1)
curl http://localhost:8000/api/health   # API

# Detener solo API
docker-compose down
Enter fullscreen mode Exit fullscreen mode

¿Por qué separado?

  • Flexibilidad: Puedes actualizar API sin tocar servicios
  • Reutilización: Qdrant/Phoenix sirven múltiples aplicaciones
  • Desarrollo: Puedes correr API local sin Docker, solo servicios en Docker

Opción Alternativa: Docker Compose Consolidado

Si prefieres levantar todo con un solo comando, puedes crear docker-compose.dev.yml en la raíz:

# docker-compose.dev.yml (en raíz del proyecto)
# Archivo opcional para desarrollo rápido

services:
  qdrant:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_storage:/qdrant/storage

  phoenix:
    image: arizephoenix/phoenix:latest
    ports:
      - "6006:6006"
      - "4317:4317"
      - "9090:9090"

  api:
    image: rj24/legal-rag-api:latest
    ports:
      - "8000:8000"
    env_file:
      - .env
    volumes:
      - ./keys/public_key.pem:/app/api/keys/public_key.pem:ro
    depends_on:
      - qdrant
      - phoenix

volumes:
  qdrant_storage:
Enter fullscreen mode Exit fullscreen mode

Uso:

# Levantar todo
docker-compose -f docker-compose.dev.yml up -d

# Detener todo
docker-compose -f docker-compose.dev.yml down
Enter fullscreen mode Exit fullscreen mode

📦 Optimización de Imágenes Docker

1. Multi-Stage Builds (Opcional)

Aunque nuestro proyecto no usa multi-stage builds actualmente, es una técnica útil para reducir tamaño:

Concepto:

  • Stage 1 (Builder): Instala todas las build tools y dependencies
  • Stage 2 (Runtime): Copia solo el .venv compilado, sin build tools

Beneficio potencial: Imagen 40-50% más pequeña (elimina gcc, make, etc.)

Por qué no lo usamos:

  • python:3.13-slim ya es bastante pequeño (~330MB total)
  • UV hace el proceso muy rápido (no vale la pena la complejidad adicional)
  • Multi-stage agrega tiempo de build

2. .dockerignore

El archivo .dockerignore previene copiar archivos innecesarios al build:

Archivo src/lus_laboris_api/.dockerignore:

# Python
__pycache__/
*.pyc
*.pyo
*.pyd

# Virtual environments
.venv/
env/
venv/

# Data and output files
data/
*.db
*.json
*.csv

# Logs
*.log

# Git files
.git/
.gitignore

# Temporary Dockerfiles
Dockerfile.*
Enter fullscreen mode Exit fullscreen mode

Beneficios:

  • Build más rápido: No copia .venv, data/, .git (pueden ser GBs)
  • Imagen más pequeña: Solo código fuente necesario
  • Context más pequeño: Docker build context ~10MB vs 500MB+

Archivos importantes que NO se ignoran:

  • api/ (código fuente)
  • pyproject.toml (dependencies)
  • uv.lock (locked versions)

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. Non-Root User es Requerido

Cloud Run y otros orchestrators requieren containers que NO corran como root. Ambos Dockerfiles usan:

RUN useradd --create-home --shell /bin/bash apiuser
USER apiuser
Enter fullscreen mode Exit fullscreen mode

Beneficios:

  • Seguridad: Limita permisos del proceso
  • Compatibilidad: Cloud Run, Kubernetes requieren non-root
  • Best practice: Principio de mínimo privilegio

🎯 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)