Patrones de Docker
Mejores prácticas de Docker y Docker Compose para desarrollo containerizado.
Docker Compose para Desarrollo Local
Pila Estándar de Aplicación Web
# docker-compose.yml
services:
app:
build:
context: .
target: dev # Usa la etapa dev del Dockerfile multietapa
ports:
- "3000:3000"
volumes:
- .:/app # Montaje bind para recarga en caliente
- /app/node_modules # Volumen anónimo -- preserva 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 correo local
image: axllent/mailpit
ports:
- "8025:8025" # Interfaz web
- "1025:1025" # SMTP
volumes:
pgdata:
redisdata:
Dockerfile de 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 (cargado 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 de base de datos
redis://redis:6379/0 # "redis" resuelve al contenedor de 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 anfitrión, no desde la red
# Omitir puertos por completo en producción -- accesible solo dentro de la red Docker
Estrategias de Volúmenes
volumes:
# Volumen nombrado: persiste entre reinicios del contenedor, gestionado por Docker
pgdata:
# Montaje bind: asigna un directorio del anfitrión al contenedor (para desarrollo)
# - ./src:/app/src
# Volumen anónimo: preserva el contenido generado por el contenedor de la anulación del montaje bind
# - /app/node_modules
Patrones Comunes
services:
app:
volumes:
- .:/app # Código fuente (montaje bind para recarga en caliente)
- /app/node_modules # Protege el node_modules del contenedor del anfitrión
- /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 de Contenedores
Endurecimiento de Dockerfile
# 1. Usa 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 subir .env a git
environment:
- API_KEY # Hereda del entorno del anfitrión
# 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 HAGAS ESTO
Arnés de Instaladores CLI Reforzados
Utiliza contenedores para probar el comportamiento del instalador con copias desechables del proyecto sin permitir que la prueba modifique el checkout fuente.
Respeta el Límite de la Plataforma
- Ejecuta 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. Ejecuta el mismo punto de entrada de prueba sin shell de forma nativa en macOS.
- Los contenedores de Windows requieren un motor Docker de Windows. Ejecuta la lógica independiente de la plataforma en un runner de CI nativo de Windows y reserva los contenedores de Windows para un anfitrión Windows.
- Mantén una matriz nativa de CI para Ubuntu/macOS/Windows para rutas específicas del anfitrión, shims de comandos, comillas y comportamiento del sistema de archivos.
No afirmes que un contenedor Linux valida el comportamiento de macOS o Windows.
Aplica el Contrato de Aislamiento
- Fija las imágenes base por digest inmutable y fija las versiones de CLI instaladas.
- Ejecuta como UID/GID numérico no root cuando los nombres de cuentas de las distribuciones difieren.
- Monta el repositorio y el proyecto fuente como solo lectura.
- Copia el proyecto fuente en un espacio de trabajo
tmpfsescribible antes de cualquier mutación. - Monta
/workspaceconnoexec, UID/GID 1000 ymode=0700para que solo el usuario del contenedor pueda inspeccionar los datos del proyecto. - Mantén 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. - Establece
read_only: true,no-new-privileges:true,cap_drop: [ALL]y unpids_limitfinito. - Mantén los servicios CLI reales predeterminados en
network_mode: none. Agrega acceso a la red solo a través de un servicio opt-in con nombre visible para una sesión de proveedor autenticado; nunca lo conviertas en un valor predeterminado accidental impulsado por el entorno. - Crea solo las rutas temporales escribibles que la herramienta necesita.
- No pases credenciales del anfitrión al contenedor de forma predeterminada.
- Por defecto, usa una ejecución en seco y permite solo los modos explícitos
dry-run,install,pluginyshell. - Usa arreglos de argumentos o
spawnSync(..., { shell: false })para ejecutores multiplataforma. Nunca interpoles rutas de proyecto en un comando shell.
Ejercita el Arnés de Configuración del Plugin ECC
Usa docker/plugin-setup/compose.yaml como la implementación de referencia. Proporciona:
fixture-testspara el suite de manifiesto de instalación, destino y ejecutor enfocado.real-clipara la imagen Linux genérica basada en Debian fijada.real-cli-ubuntupara la imagen Ubuntu fijada.
Valida el modelo de Compose antes de construir:
docker compose -f docker/plugin-setup/compose.yaml config --quiet
Construye ambas imágenes Linux reales:
docker compose -f docker/plugin-setup/compose.yaml \
build real-cli real-cli-ubuntu
Ejecuta el flujo predeterminado seguro 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 desde el 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 la asignación package.json bin.ecc confinada, luego invoca el ejecutable ecc extraído. El tiempo de ejecución permanece en network_mode: none, no ejecuta scripts del ciclo de vida del paquete y no depende de los node_modules del anfitrión; sus dependencias de producción exactas fijadas 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
Inicia un contenedor separado sin --rm para que salir de un terminal no 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. Confirma que se está ejecutando y luego emite el contrato de datos v1 del lado Docker del abridor de terminal:
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 executable y argv (más contractVersion: 1): el ejecutable es docker, y argv comienza con exec, -it y -w. Pasa esos datos a la habilidad separada de abridor de terminal cuando esté instalada. Este arnés Docker deliberadamente no importa un adaptador de terminal, interpola un comando shell ni gestiona un proceso GUI del anfitrión. Hasta entonces, abre el mismo PTY en el terminal del anfitrión actual directamente:
docker exec -it -w /workspace/project ecc-plugin-shell bash
Sal del shell sin detener el contenedor separado. Reconecta con el mismo comando docker exec -it. Cuando termines, elimina el contenedor nombrado exacto y sus recursos del proyecto 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 anfitrión están ausentes de forma predeterminada 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 necesite una red, construye real-cli primero y luego haz opt-in visiblemente con docker compose --profile networked run real-cli-networked shell. Prefiere autenticar dentro de esa sesión desechable. Si una ejecución de CI debe heredar una credencial de entorno del anfitrión, haz que ese opt-in sea en la invocación con una bandera explícita --env NAME de Compose, entiende que el valor es inspeccionable y puede ser exfiltrado durante la vida útil del contenedor, y elimina el contenedor nombrado exacto inmediatamente después.
Ejecuta el mismo suite enfocado de forma nativa en el anfitrión:
npm run test:plugin-setup-platform
Inspecciona la identidad y el entorno producidos antes de confiar en la imagen:
docker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu
Limpia 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 logs
docker compose logs -f app # Seguir logs 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 usados
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 la red
docker network ls
docker network inspect <project>_default
Anti-Patrones
# MAL: Usar :latest en imágenes base
FROM node:latest
# MAL: Ejecutar como root
USER root
# MAL: Almacenar secretos en variables de entorno en la imagen
ENV API_KEY=sk-proj-xxxxx
# MAL: Exponer puertos de base de datos a 0.0.0.0 en producción
ports:
- "5432:5432"
# MAL: No usar healthchecks en servicios dependientes
depends_on:
- db
Prácticas Recomendadas
Dockerfile
- Usa imágenes base oficiales y ligeras (alpine, slim).
- Fija versiones específicas de imagen; nunca uses
:latest. - Usa builds multietapa para minimizar el tamaño de la imagen final.
- Ejecuta aplicaciones como un usuario no root.
- Usa
COPYen lugar deADDa menos que necesites descargas de URLs o extracción automática de tarballs. - Combina
RUNlayers para reducir el número de capas. - Usa
--no-cacheo--no-install-recommendspara reducir el tamaño de la imagen. - Añade
HEALTHCHECKa la imagen de producción.
Docker Compose
- Usa
depends_onconcondition: service_healthypara servicios con healthcheck. - Limita los recursos del contenedor con
deploy.resources.limitsen producción. - Usa archivos
.envpara configuraciones específicas del entorno. - Usa perfiles de Compose para diferentes escenarios (desarrollo, pruebas, producción).
- Mantén los datos persistentes en volúmenes nombrados.
- Usa
restart: alwaysorestart: unless-stoppedpara servicios de producción.