Multi-stage builds
El problema
Sección titulada «El problema»Compilar una app (TypeScript → JS, un frontend con Vite, un binario de Go) necesita herramientas — compilador, devDependencies, a veces el código fuente completo. Pero para correr la app compilada, nada de eso hace falta. Si todo va en un solo FROM, la imagen final carga con herramientas de build que solo sirvieron una vez y después son peso muerto.
La solución: varias etapas, una sola imagen final
Sección titulada «La solución: varias etapas, una sola imagen final»# Etapa 1: build — tiene TypeScript, devDependencies, todo el código fuenteFROM node:20-alpine AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run build
# Etapa 2: runtime — solo lo necesario para correrFROM node:20-alpineWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=devCOPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]AS buildnombra la primera etapa.COPY --from=build /app/dist ./distcopia solo el resultado compilado desde la etapabuildhacia la etapa final — el compilador, el código TypeScript fuente y lasdevDependenciesde la etapabuildnunca llegan a la imagen final.
Por qué importa
Sección titulada «Por qué importa»| Sin multi-stage | Con multi-stage | |
|---|---|---|
| Tamaño de imagen | Grande (incluye devDependencies, source, herramientas de build) | Chica (solo runtime + artefactos) |
| Superficie de ataque | Mayor (más paquetes = más CVEs potenciales) | Menor |
| Velocidad de deploy/pull | Más lenta (imagen más pesada) | Más rápida |
Ejemplo con más de dos etapas
Sección titulada «Ejemplo con más de dos etapas»FROM node:20-alpine AS depsWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci
FROM deps AS buildCOPY . .RUN npm run build
FROM node:20-alpine AS runtimeWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=devCOPY --from=build /app/dist ./distCMD ["node", "dist/server.js"]Cada etapa puede referenciar otra etapa anterior como base (FROM deps AS build) — reutiliza su cache igual que cualquier otra imagen.
Consideraciones
Sección titulada «Consideraciones»- Solo la última etapa del Dockerfile termina siendo la imagen final que
docker buildproduce — las etapas intermedias existen solo durante el build (aunque quedan cacheadas para builds futuros). - Se puede construir una etapa específica sin llegar al final, útil para debug:
docker build --target build -t mi-app-build ..