Patrones de Docker
Mejores prácticas de Docker y Docker Compose para el desarrollo en contenedores.
Docker Compose para Desarrollo Local
Pila Estándar de Aplicaciones Web
# docker-compose.yml
services:
app:
build:
context: .
target: dev # Usa la etapa dev del Dockerfile de múltiples etapas
ports:
- "3000:3000"
volumes:
- .:/app # Montaje enlazado para recarga en caliente
- /app/node_modules # Volumen anónimo: conserva las dependencias del contenedor
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/app_dev
- REDIS_URL=redis://redis:6379/0
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
mailpit: # Pruebas de email local
image: axllent/mailpit
ports:
- "8025:8025" # Interfaz web
- "1025:1025" # SMTP
volumes:
pgdata:
redisdata:
Dockerfile para Desarrollo vs Producción
# Etapa: dependencias
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Etapa: dev (recarga en caliente, herramientas de depuración)
FROM node:22-alpine AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Etapa: build
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production
# Etapa: producción (imagen mínima)
FROM node:22-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
USER appuser
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
Archivos de Anulación
# docker-compose.override.yml (se carga automáticamente, solo para desarrollo)
services:
app:
environment:
- DEBUG=app:*
- LOG_LEVEL=debug
ports:
- "9229:9229" # Depurador de Node.js
# docker-compose.prod.yml (explícito para producción)
services:
app:
build:
target: production
restart: always
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
# Desarrollo (carga automática de anulación)
docker compose up
# Producción
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Redes
Descubrimiento de Servicios
Los servicios en la misma red de Compose se resuelven por nombre de servicio:
# Desde el contenedor "app":
postgres://postgres:postgres@db:5432/app_dev # "db" resuelve al contenedor db
redis://redis:6379/0 # "redis" resuelve al contenedor redis
Redes Personalizadas
services:
frontend:
networks:
- frontend-net
api:
networks:
- frontend-net
- backend-net
db:
networks:
- backend-net # Solo accesible desde api, no desde frontend
networks:
frontend-net:
backend-net:
Exponer Solo lo Necesario
services:
db:
ports:
- "127.0.0.1:5432:5432" # Solo accesible desde el host, no desde la red
# Omitir puertos por completo en producción: accesible solo dentro de la red Docker
Estrategias de Volúmenes
volumes:
# Volumen con nombre: persiste entre reinicios del contenedor, gestionado por Docker
pgdata:
# Montaje enlazado: mapea un directorio del host al contenedor (para desarrollo)
# - ./src:/app/src
# Volumen anónimo: preserva el contenido generado por el contenedor de la anulación del montaje enlazado
# - /app/node_modules
Patrones Comunes
services:
app:
volumes:
- .:/app # Código fuente (montaje enlazado para recarga en caliente)
- /app/node_modules # Protege node_modules del contenedor del host
- /app/.next # Protege la caché de build
db:
volumes:
- pgdata:/var/lib/postgresql/data # Datos persistentes
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql # Scripts de inicialización
Seguridad del Contenedor
Endurecimiento del Dockerfile
# 1. Usar etiquetas específicas (nunca :latest)
FROM node:22.12-alpine3.20
# 2. Ejecutar como no root
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
USER app
# 3. Eliminar capacidades (en compose)
# 4. Sistema de archivos raíz de solo lectura cuando sea posible
# 5. Sin secretos en las capas de la imagen
Seguridad en Compose
services:
app:
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /app/.cache
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Solo si se enlazan puertos < 1024
Gestión de Secretos
# BIEN: Usar variables de entorno (inyectadas en tiempo de ejecución)
services:
app:
env_file:
- .env # Nunca hacer commit de .env en git
environment:
- API_KEY # Hereda del entorno del host
# BIEN: Secretos de Docker (modo Swarm)
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
secrets:
- db_password
# MAL: Hardcodeado en la imagen
# ENV API_KEY=sk-proj-xxxxx # NUNCA HACER ESTO
Arnés de Instalación de CLI Reforzado
Use contenedores para probar el comportamiento del instalador con copias desechables del proyecto sin permitir que la prueba mute el checkout fuente.
Respetar el Límite de la Plataforma
- Ejecute contenedores reales para distribuciones de Linux como Debian y Ubuntu.
- macOS no puede ejecutarse como un contenedor Docker porque Docker comparte un kernel de Linux. Ejecute el mismo punto de entrada de prueba libre de shell de forma nativa en macOS.
- Los contenedores de Windows requieren un motor Docker de Windows. Ejecute la lógica independiente de la plataforma en un runner de CI de Windows nativo y reserve los contenedores de Windows para un host de Windows.
- Mantenga una matriz de CI nativa de Ubuntu/macOS/Windows para rutas específicas del host, shims de comandos, comillas y comportamiento del sistema de archivos.
No afirme que un contenedor Linux valida el comportamiento de macOS o Windows.
Hacer Cumplir el Contrato de Aislamiento
- Fije las imágenes base por digest inmutable y fije las versiones de CLI instaladas.
- Ejecute como un UID/GID numérico no root cuando los nombres de cuenta de la distribución difieran.
- Monte el repositorio y el proyecto fuente como solo lectura.
- Copie el proyecto fuente en un espacio de trabajo
tmpfsescribible antes de cualquier mutación. - Monte
/workspaceconnoexec, UID/GID 1000, ymode=0700para que solo el usuario del contenedor pueda inspeccionar los datos del proyecto. - Mantenga la caché ejecutable de npm y npx en
NPM_CONFIG_CACHE=/tmp/npm-cacheen el montaje ejecutable/tmp. Su tamaño predeterminado es 2 GiB y se puede ajustar conECC_TMPFS_SIZE;ECC_WORKSPACE_SIZEcontrola por separado el montaje del espacio de trabajo privado. - Establezca
read_only: true,no-new-privileges:true,cap_drop: [ALL], y unpids_limitfinito. - Mantenga los servicios de CLI reales predeterminados en
network_mode: none. Agregue acceso a la red solo a través de un servicio de suscripción opcional con nombre visible para una sesión de proveedor autenticado; nunca lo convierta en un valor predeterminado accidental impulsado por el entorno. - Cree solo las rutas temporales escribibles que la herramienta necesita.
- No pase credenciales del host al contenedor por defecto.
- Por defecto, realice una ejecución en seco y permita solo los modos explícitos
dry-run,install,pluginyshell. - Use arreglos de argumentos o
spawnSync(..., { shell: false })para runners multiplataforma. Nunca interpole rutas de proyecto en un comando shell.
Ejercitar el Arnés de Configuración del Plugin ECC
Use docker/plugin-setup/compose.yaml como la implementación de referencia. Proporciona:
fixture-testspara el manifiesto de instalación enfocado, destino y suite de ejecutores.real-clipara la imagen genérica de Linux basada en Debian fijada.real-cli-ubuntupara la imagen de Ubuntu fijada.
Valide el modelo de Compose antes de construir:
docker compose -f docker/plugin-setup/compose.yaml config --quiet
Construya ambas imágenes de Linux reales:
docker compose -f docker/plugin-setup/compose.yaml \
build real-cli real-cli-ubuntu
Ejecute el flujo seguro predeterminado en cada imagen:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli dry-run
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli-ubuntu dry-run
La ejecución en seco ejecuta el contrato de comando público actual:
ecc install --profile core --target claude-project --dry-run --json
Antes de que se ejecute ese comando, el contenedor crea un artefacto npm empaquetado localmente a partir del checkout de solo lectura con npm pack --ignore-scripts. Extrae el tarball auto-creado en /tmp, valida el nombre del paquete ecc-universal, los manifiestos de instalación requeridos y el mapeo bin.ecc de package.json confinado, luego invoca el ejecutable ecc extraído. El tiempo de ejecución permanece en network_mode: none, no ejecuta scripts de ciclo de vida del paquete y no depende de node_modules del host; sus dependencias de producción fijadas exactas ya están presentes en la imagen.
El arnés rechaza un plan vacío, un destino que no sea claude-project, cualquier operación fuera de /workspace/project/.claude, o cualquier ejecución en seco que cree el directorio de destino. install realiza la aplicación aislada dos veces, verifica su estado de instalación gestionado, lista el destino instalado y ejecuta doctor.
Iniciar, Abrir, Reconectar y Limpiar una Sesión Nombrada
Inicie un contenedor separado sin --rm para que al salir de una terminal no se elimine la sesión:
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
run --detach --name ecc-plugin-shell real-cli shell
El contenedor copia el fixture de solo lectura al directorio privado estable /workspace/project. Confirme que se está ejecutando, luego emita el lado Docker del contrato de datos del abridor de terminal v1:
docker inspect --format '{{.State.Running}}' ecc-plugin-shell
node docker/plugin-setup/interactive-plan.js \
--container ecc-plugin-shell \
--workdir /workspace/project \
--json \
-- bash
El resultado JSON tiene exactamente un límite de executable y argv (más contractVersion: 1): el ejecutable es docker, y argv comienza con exec, -it y -w. Pase esos datos a la skill del abridor de terminal separada cuando esté instalada. Este arnés de Docker deliberadamente no importa un adaptador de terminal, interpola un comando shell ni gestiona un proceso GUI del host. Hasta entonces, abra la misma PTY en la terminal del host actual directamente:
docker exec -it -w /workspace/project ecc-plugin-shell bash
Salga del shell sin detener el contenedor separado. Reconecte con el mismo comando docker exec -it. Cuando termine, elimine el contenedor nombrado exacto y sus recursos del proyecto de Compose:
docker rm --force ecc-plugin-shell
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
down --remove-orphans
Las credenciales del host están ausentes por defecto y los directorios de credenciales nunca se montan. El servicio predeterminado tampoco tiene acceso a la red. Cuando una sesión de proveedor autenticado realmente necesita una red, primero construya real-cli y luego opte visiblemente con docker compose --profile networked run real-cli-networked shell. Prefiera autenticarse dentro de esa sesión desechable. Si una ejecución de CI debe heredar una credencial de entorno del host, hágalo optativo en la invocación con una bandera explícita --env NAME de Compose, entienda que el valor es inspeccionable y puede ser exfiltrado durante la vida útil del contenedor, y elimine el contenedor nombrado exacto inmediatamente después.
Ejecute la misma suite enfocada de forma nativa en el host:
npm run test:plugin-setup-platform
Inspeccione la identidad y el entorno producidos antes de confiar en la imagen:
docker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu
Limpie cada proyecto de prueba nombrado sin eliminar volúmenes o imágenes no relacionados:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
.dockerignore
node_modules
.git
.env
.env.*
dist
coverage
*.log
.next
.cache
docker-compose*.yml
Dockerfile*
README.md
tests/
Depuración
Comandos Comunes
# Ver registros
docker compose logs -f app # Seguir registros de app
docker compose logs --tail=50 db # Últimas 50 líneas de db
# Ejecutar comandos en un contenedor en ejecución
docker compose exec app sh # Shell dentro de app
docker compose exec db psql -U postgres # Conectar a postgres
# Inspeccionar
docker compose ps # Servicios en ejecución
docker compose top # Procesos en cada contenedor
docker stats # Uso de recursos
# Reconstruir
docker compose up --build # Reconstruir imágenes
docker compose build --no-cache app # Forzar reconstrucción completa
# Limpiar
docker compose down # Detener y eliminar contenedores
docker compose down -v # También eliminar volúmenes (DESTRUCTIVO)
docker system prune # Eliminar imágenes/contenedores no utilizados
Depuración de Problemas de Red
# Verificar resolución DNS dentro del contenedor
docker compose exec app nslookup db
# Verificar conectividad
docker compose exec app wget -qO- http://api:3000/health
# Inspeccionar red
docker network ls
docker network inspect <project>_default
Anti-Patrones
# MAL: Uso de `latest` sin control de versiones
# FROM node:latest
# MAL: Ejecutar como root en producción
# USER root
# MAL: Exponer puertos de base de datos a todo el mundo
# ports:
# - "0.0.0.0:5432:5432"
# MAL: Hardcodear secretos en variables de entorno del Dockerfile
# ENV DATABASE_URL=postgres://user:password@host:5432/db
# MAL: Montar todo el contexto de build como volumen de desarrollo
# volumes:
# - .:/app
# (puede sobrescribir node_modules del contenedor)