Capítulo 2 de 3 · Docker desde cero
Escribir un Dockerfile decente
Capas, caché y orden de las instrucciones. Por qué tu build tarda tres minutos y cómo dejarlo en diez segundos.
Cada instrucción de un Dockerfile crea una capa, y Docker reutiliza esa capa si ni la instrucción
ni lo que entra en ella han cambiado desde la última vez. Parece poca cosa, pero entender bien esa
regla es casi todo lo que hace falta para escribir buenos Dockerfiles. El resto son detalles.
Lo que sale a la primera
FROM node:22
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
Funciona, y es lo que escribe casi todo el mundo la primera vez. El problema es que COPY . .
invalida la caché en cuanto cambias cualquier fichero del proyecto, y como npm install viene
justo después, se vuelve a ejecutar en cada build. De ahí los tres minutos.
Copia primero lo que cambia menos
La idea es ordenar las instrucciones por frecuencia de cambio, de menos a más:
FROM node:22-slim
WORKDIR /app
# Solo el manifiesto: si el código cambia, esta capa sigue siendo válida
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
CMD ["node", "src/server.js"]
Ahora npm ci solo se repite cuando cambian las dependencias, que es mucho menos a menudo que el
código.
Deja fuera lo que no debe entrar
Sin un .dockerignore, el contexto de build se lleva node_modules, .git y tus ficheros .env.
Aparte de que tarda más, es una forma estupenda de meter secretos en una imagen sin darte cuenta.
node_modules
.git
.env*
dist
*.log
Build multi-etapa
La idea es compilar en una etapa que tiene todas las herramientas y luego copiar solo el resultado a una imagen mínima:
FROM node:22 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
La imagen final no lleva el compilador, ni las dependencias de desarrollo, ni el código fuente. Solo lo que hace falta para ejecutar.
| Enfoque | Tamaño típico |
|---|---|
node:22 + npm install |
~1.1 GB |
node:22-slim + capas ordenadas |
~280 MB |
Multi-etapa sobre slim |
~140 MB |
Tres cosas que se olvidan siempre
USER. Por defecto el proceso corre como root dentro del contenedor, y no hay ningún motivo para eso. La imagennodeya trae un usuario llamadonode, úsalo.- Las señales.
CMD ["node", "server.js"], la forma exec, recibeSIGTERMy puede cerrar limpiamente. La forma shell,CMD node server.js, no la recibe, y tu contenedor se quedará diez segundos colgado en cada despliegue hasta que Docker lo mate. HEALTHCHECK. Es lo que permite al orquestador saber si el proceso está de verdad listo para recibir tráfico, y no solo arrancado.
En el siguiente capítulo publicamos esta imagen en un registro y la desplegamos.