Flujos de trabajo con Docker que realmente funcionan
Deja de pelearte con Docker en local: builds multi-stage, bind mounts, overrides de Compose y los patrones que hacen que los contenedores se sientan nativos.

Docker debería hacer que tu entorno sea reproducible. En la práctica, muchos equipos terminan con Dockerfiles que funcionan en CI, se rompen en local y requieren un ritual de conocimiento tribal para poder ejecutarlos. La herramienta es poderosa; la brecha está en cómo se usa.
El build multi-stage no es negociable
Un Dockerfile de una sola etapa que copia todo en la imagen es la causa raíz de la mayoría de las frustraciones con Docker. Genera imágenes grandes, lentas de reconstruir e imposibles de optimizar tanto para desarrollo como para producción.
# ❌ Single-stage — ships dev dependencies, source maps, everything
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["node", "dist/index.js"]
# ✅ Multi-stage — lean production image, proper layer caching
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --frozen-lockfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./
USER node
CMD ["node", "dist/index.js"]La etapa deps se cachea por separado. Si package.json no cambia, Docker reutiliza la capa: tu npm ci solo se ejecuta cuando las dependencias cambian de verdad.
Overrides de Docker Compose para desarrollo local
Ejecutar el mismo docker-compose.yml en desarrollo y producción obliga a compromisos dolorosos. Mejor usa un archivo base más overrides específicos por entorno.
# docker-compose.yml (base — shared config)
services:
api:
build:
context: .
target: deps # stop at deps stage for dev
environment:
DATABASE_URL: postgres://postgres:password@db:5432/app
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5# docker-compose.override.yml (dev — auto-merged by Compose)
services:
api:
build:
target: deps
command: npm run dev # hot reload in dev
volumes:
- .:/app # bind mount source
- /app/node_modules # keep container's node_modules
environment:
NODE_ENV: development
ports:
- "3000:3000"
- "9229:9229" # Node debugger# docker-compose.prod.yml (production — explicit override)
services:
api:
build:
target: runner
restart: unless-stopped
environment:
NODE_ENV: productionEjecuta docker compose up en local (fusiona el override automáticamente), docker compose -f docker-compose.yml -f docker-compose.prod.yml up en producción.
Bind mounts y el truco de node_modules
El punto de dolor más común en desarrollo local: haces bind mount de tu código fuente en el contenedor, pero ahora los node_modules del contenedor (compilados para Linux) se sobrescriben con los node_modules de tu Mac (compilados para macOS/Windows).
La solución es un volumen anónimo que enmascare el bind mount en node_modules:
volumes:
- .:/app # sync source code
- /app/node_modules # hide host's node_modules, keep container'sEl volumen vacío con destino /app/node_modules tiene prioridad sobre el bind mount padre para esa ruta específica. Tu código fuente se sincroniza; tus binarios específicos de plataforma permanecen intactos.
Caché de capas en CI
Los tiempos de build se acumulan. Un Dockerfile mal ordenado vuelve a descargar todas las dependencias en cada cambio de código. La regla: ordena las capas de menos cambiadas a más cambiadas.
FROM node:22-alpine AS builder
WORKDIR /app
# 1. Copy only the manifest — cached until deps change
COPY package.json package-lock.json ./
RUN npm ci --frozen-lockfile
# 2. Copy generated/config files that rarely change
COPY tsconfig.json ./
COPY prisma ./prisma
RUN npx prisma generate
# 3. Copy source — changes on every commit
COPY src ./src
RUN npm run buildEn GitHub Actions, usa cache-from y cache-to con el driver de registro para persistir la caché de capas entre ejecuciones:
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
target: runner
cache-from: type=registry,ref=ghcr.io/org/app:cache
cache-to: type=registry,ref=ghcr.io/org/app:cache,mode=max
tags: ghcr.io/org/app:${{ github.sha }}Health checks y apagado graceful
Los servicios que declaran health checks permiten que Compose (y Kubernetes) esperen a que estén listos antes de arrancar los servicios dependientes. Saltárselos causa condiciones de carrera que se manifiestan como fallos de test inconsistentes.
# In your Dockerfile
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1El apagado graceful importa en los contenedores porque docker stop envía SIGTERM. Si tu proceso no lo maneja, Docker lo mata por la fuerza tras el timeout, dejando caer peticiones en vuelo.
// Handle SIGTERM for graceful shutdown
const server = app.listen(3000);
async function shutdown() {
console.log("SIGTERM received, shutting down gracefully");
server.close(async () => {
await db.end(); // close DB connections
process.exit(0);
});
// Force exit if graceful shutdown takes too long
setTimeout(() => process.exit(1), 10_000);
}
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);Puntos clave
- Los builds multi-stage son la base — separa las etapas de instalación de dependencias, build y runtime para imágenes más pequeñas y mejor caché
- Usa overrides de Compose, no variables de entorno, para las diferencias entre dev y prod — el patrón de override mantiene ambos entornos explícitos y revisables
- El truco del volumen anónimo para
node_modulesevita colisiones de binarios específicos de plataforma con bind mounts - Ordena las capas del Dockerfile de menos cambiadas a más cambiadas — copiar los manifiestos de paquetes antes que el código fuente es la optimización de caché con mayor retorno de inversión
- Los health checks y el apagado graceful son requisitos de producción —
depends_oncon condiciones de salud previene condiciones de carrera; el manejo deSIGTERMevita peticiones perdidas


