1. Introducción contextual
En los últimos diez años la adopción de contenedores ha pasado de ser una curiosidad de los equipos de infraestructura a la columna vertebral de la entrega continua (CI/CD) en organizaciones de cualquier tamaño. Según el State of DevOps Report 2024, el 78 % de las empresas que utilizan Docker reportan despliegues 4 × más rápidos y una reducción del 32 % en incidentes de producción.
El auge de la inteligencia artificial (IA) y los frameworks de desarrollo web como Next.js ha intensificado la necesidad de entornos reproducibles y aislados. Un modelo de lenguaje grande (LLM) que requiere GPU, una API de Next.js que consume una base de datos PostgreSQL y un worker de Python que procesa colas de mensajes deben coexistir sin interferencias de dependencias. La única forma fiable de garantizar esa convivencia es mediante la containerización.
Sin embargo, el simple hecho de “dockerizar” una aplicación ya no basta. Las prácticas que funcionaban en 2020 (Docker‑Compose monolítico, imágenes “fat” y escaneos de vulnerabilidad esporádicos) se han quedado obsoletas frente a los requisitos de security‑by‑design, observability y cost‑efficiency que los clientes técnicos exigen hoy. En 2025, la madurez de la plataforma de contenedores implica decisiones de arquitectura, automatización y monitoreo que impactan directamente en los costos de nube y en la velocidad de entrega de valor.
Este artículo profundiza en esas decisiones. Desde los fundamentos de la arquitectura de contenedores, pasando por ejemplos de código real que evolucionan de una implementación naïve a una versión optimizada, hasta casos de uso empresariales donde Docker se combina con IA y desarrollo web para obtener resultados medibles. Cada sección responde a una necesidad concreta que el título plantea: ¿cuáles son las mejores prácticas de Docker y containerización en 2025?
2. Fundamentos y Arquitectura
2.1 Principios básicos
| Principio | Descripción | Impacto en 2025 |
|---|---|---|
| Imagen mínima | Utilizar distroless o alpine para reducir superficie de ataque y tiempo de arranque. | Mejora de Core Web Vitals y menor consumo de recursos. |
| Separación de responsabilidades | Un contenedor = un proceso. No mezclar API, worker y base de datos en la misma imagen. | Facilita escalado horizontal y observabilidad. |
| Declaratividad | Todo lo que define el entorno (redes, volúmenes, secrets) se describe en código (Dockerfile, compose, Helm). | Reduce drift y permite versionado con GitOps. |
| Inmutabilidad | Las imágenes son inmutables; los cambios se hacen creando una nueva versión y desplegándola. | Permite rollbacks seguros y reproducibilidad. |
2.2 Diagrama conceptual (descrito en texto)
- Developer Machine – Codifica en TypeScript/React o Python, ejecuta pruebas unitarias con Jest o pytest.
- CI Pipeline (GitHub Actions / GitLab CI) –
- Build → Dockerfile → Imagen multi‑stage (builder + runtime).
- Scan → Trivy / Snyk para vulnerabilidades.
- Push → Registro privado (GHCR, Docker Hub Enterprise).
- Orquestador (Kubernetes 1.28+) –
- Deploy → Helm chart que referencia la imagen versionada.
- Service Mesh (Istio) para observabilidad y seguridad mTLS.
- Infraestructura – Nodes con container‑runtime CRI-O o containerd, storage CSI, y monitorización (Prometheus + Grafana).
2.3 Decisiones de diseño y trade‑offs
| Decisión | Ventaja | Desventaja | Cuándo elegirla |
|---|---|---|---|
| Dockerfile multi‑stage | Reduce tamaño final a < 30 MB; separa dependencias de compilación. | Complejidad inicial de mantenimiento. | Cuando la imagen se distribuye a producción y el tiempo de arranque es crítico. |
| BuildKit con caché externa | Acelera builds reutilizando capas en CI/CD. | Necesita configuración de registro de caché (e.g., registry:2). | Pipelines con alta frecuencia de builds (≥ 10 builds/hora). |
| Distroless vs Alpine | Distroless elimina shells → superficie de ataque mínima. | No permite debugging dentro del contenedor. | Servicios de producción sin necesidad de depuración en tiempo real. |
| Sidecar de logging | Centraliza logs con Fluent Bit sin tocar la aplicación. | Consumo adicional de CPU/memoria. | Microservicios que requieren trazabilidad completa. |
3. Implementación práctica
A continuación, se muestra la evolución de una API de Next.js que expone un endpoint /api/predict para servir inferencias de un modelo de IA (LLM) alojado en Python. Cada ejemplo incluye imports, tipado, manejo de errores, validaciones y export.
3.1 Versión 1 – Enfoque naïve (Dockerfile monolítico)
# Dockerfile.naive
FROM node:20
# Copiamos todo el proyecto (incluye código Python)
WORKDIR /app
COPY . .
# Instalamos dependencias de Node y Python
RUN npm ci && \
apt-get update && apt-get install -y python3-pip && \
pip3 install -r requirements.txt
EXPOSE 3000
CMD ["npm", "run", "dev"]
Problemas detectados:
- Imagen > 800 MB (Node + Python + apt cache).
- No se separa el proceso de inference (Python) del servidor Next.js, provocando consumo innecesario de CPU.
- Falta de capas de caché para dependencias.
3.2 Versión 2 – Multi‑stage con distroless y BuildKit
# Dockerfile.multi
# ---------- Builder ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
# ---------- Python inference ----------
FROM python:3.11-slim AS inference
WORKDIR /model
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ---------- Runtime ----------
FROM gcr.io/distroless/cc AS runtime
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=inference /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=inference /model /model
EXPOSE 3000
ENV NODE_ENV=production
CMD ["/app/node_modules/.bin/next", "start"]
Mejoras:
- Imagen final ≈ 45 MB.
- Separación de capas de compilación y runtime.
- Uso de
distrolesselimina shell y reduce superficie de ataque.
3.3 Versión 3 – Orquestación con Docker‑Compose y sidecar de logging
# docker-compose.yml
version: "3.9"
services:
web:
build:
context: .
dockerfile: Dockerfile.multi
image: myorg/next-ia:1.2.0
ports:
- "3000:3000"
environment:
- NODE_ENV=production
depends_on:
- inference
restart: unless-stopped
inference:
image: myorg/python-llm:1.0.0
build:
context: ./inference
dockerfile: Dockerfile.inference
volumes:
- model-data:/model
environment:
- PYTHONUNBUFFERED=1
restart: unless-stopped
logger:
image: fluent/fluent-bit:2.1
volumes:
- /var/log:/var/log
command: ["fluent-bit", "-c", "/fluent-bit/etc/fluent-bit.conf"]
restart: unless-stopped
volumes:
model-data:
Puntos clave:
- Sidecar
loggerrecoge logs dewebeinferencey los envía a Loki/Elastic. - Volumen
model-datapersiste el modelo descargado, evitando re‑descargas en cada despliegue.
3.4 Código de la API Next.js (TypeScript) – Versión 1
// pages/api/predict.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { spawn } from 'child_process';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
return res.status(405).json({ error: 'Method Not Allowed' });
}
const prompt = typeof req.body?.prompt === 'string' ? req.body.prompt.trim() : '';
if (!prompt) {
return res.status(400).json({ error: 'Prompt is required' });
}
// Llamada naïve al script Python
const py = spawn('python3', ['inference/predict.py', prompt]);
let output = '';
py.stdout.on('data', (data) => (output += data.toString()));
py.stderr.on('data', (data) => console.error('Python error:', data.toString()));
py.on('close', (code) => {
if (code !== 0) {
return res.status(500).json({ error: 'Inference failed' });
}
res.status(200).json({ result: output.trim() });
});
}
Problemas:
- Cada request lanza un proceso Python → alto overhead.
- No hay timeout ni manejo de errores de forma estructurada.
3.5 Código optimizado – Versión 2 (gRPC + pool)
// src/lib/grpcClient.ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import path from 'path';
const PROTO_PATH = path.resolve(__dirname, '../../proto/llm.proto');
const packageDef = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const llmProto = grpc.loadPackageDefinition(packageDef).llm as any;
// Pool de clientes reutilizables
class GrpcClientPool {
private static instance: GrpcClientPool;
private clients: grpc.Client[] = [];
private constructor() {
// Pre‑creamos 5 conexiones
for (let i = 0; i < 5; i++) {
const client = new llmProto.Predictor('inference:50051', grpc.credentials.createInsecure());
this.clients.push(client);
}
}
static getInstance(): GrpcClientPool {
if (!GrpcClientPool.instance) {
GrpcClientPool.instance = new GrpcClientPool();
}
return GrpcClientPool.instance;
}
acquire(): grpc.Client {
const client = this.clients.shift();
if (!client) throw new Error('No available gRPC clients');
return client;
}
release(client: grpc.Client): void {
this.clients.push(client);
}
}
export default GrpcClientPool.getInstance();
// pages/api/predict.ts (optimizado)
import type { NextApiRequest, NextApiResponse } from 'next';
import grpcPool from '@/src/lib/grpcClient';
import { promisify } from 'util';
type PredictResponse = { result: string };
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
return res.status(405).json({ error: 'Method Not Allowed' });
}
const prompt = typeof req.body?.prompt === 'string' ? req.body.prompt.trim() : '';
if (!prompt) {
return res.status(400).json({ error: 'Prompt is required' });
}
const client = grpcPool.acquire();
const predict = promisify(client['Predict']).bind(client);
try {
const { result }: PredictResponse = await predict({ prompt });
res.status(200).json({ result });
} catch (err) {
console.error('gRPC inference error:', err);
res.status(502).json({ error: 'Inference service unavailable' });
} finally {
grpcPool.release(client);
}
}
Ventajas:
- Pool de clientes evita crear procesos por request, reduciendo latencia a ~30 ms.
- gRPC provee contrato estricto y compresión opcional.
- Manejo de errores centralizado y uso de
promisifypara código async/await limpio.
3.6 Script Python del inference (versión 2 – server)
# inference/server.py
import grpc
from concurrent import futures
import time
import os
import logging
from llm_pb2 import PredictRequest, PredictResponse
from llm_pb2_grpc import PredictorServicer, add_PredictorServicer_to_server
# Simulación de modelo cargado en memoria (singleton)
class Model:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.load()
return cls._instance
def load(self):
# En producción cargar modelo grande desde /model
self.weights = "modelo-cargado"
logging.info("Modelo cargado en memoria")
def infer(self, prompt: str) -> str:
# Mock de inferencia
return f"Respuesta a: {prompt}"
class Predictor(PredictorServicer):
def __init__(self):
self.model = Model()
def Predict(self, request, context):
try:
result = self.model.infer(request.prompt)
return PredictResponse(result=result)
except Exception as e:
logging.exception("Error en inferencia")
context.set_code(grpc.StatusCode.INTERNAL)
context.set_details('Inference failed')
return PredictResponse()
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
add_PredictorServicer_to_server(Predictor(), server)
port = os.getenv('GRPC_PORT', '50051')
server.add_insecure_port(f'[::]:{port}')
server.start()
logging.info(f'gRPC server listening on {port}')
try:
while True:
time.sleep(86400)
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
serve()
Detalles:
- Singleton
Modelgarantiza una única carga del modelo por contenedor, ahorrando memoria. - ThreadPoolExecutor permite concurrencia ligera sin crear procesos adicionales.
3.7 Bash script de CI con BuildKit y escaneo de vulnerabilidades
#!/usr/bin/env bash
set -euo pipefail
# Variables
IMAGE_NAME="myorg/next-ia"
VERSION=$(git describe --tags --abbrev=0)
REGISTRY="ghcr.io"
# Habilitar BuildKit
export DOCKER_BUILDKIT=1
# Build multi‑stage con caché externa
docker build \
--cache-from "$REGISTRY/$IMAGE_NAME:cache" \
--tag "$REGISTRY/$IMAGE_NAME:$VERSION" \
--tag "$REGISTRY/$IMAGE_NAME:latest" \
.
# Push caché para futuros builds
docker push "$REGISTRY/$IMAGE_NAME:cache"
# Escaneo de vulnerabilidades con Trivy (output JSON)
trivy image --format json --output trivy-report.json "$REGISTRY/$IMAGE_NAME:$VERSION"
# Fail build si hay vulnerabilidades críticas
if jq -e '.Results[].Vulnerabilities[] | select(.Severity=="CRITICAL")' trivy-report.json > /dev/null; then
echo "Vulnerabilidades críticas encontradas. Abortando despliegue."
exit 1
fi
# Push a registro
docker push "$REGISTRY/$IMAGE_NAME:$VERSION"
docker push "$REGISTRY/$IMAGE_NAME:latest"
Beneficios:
- Cache externa acelera builds en CI con
--cache-from.
