Saltar al contenido principal

Imágenes preparadas para producción

Una imagen que funciona no es automáticamente adecuada para distribuir y ejecutar en cualquier entorno real. Antes de mejorarla necesitamos preguntar qué contiene, qué necesita durante el build, qué necesita al arrancar y con qué privilegios trabaja.

No existe un Dockerfile universal «production ready». Compatibilidad, mantenimiento, seguridad, diagnóstico, tamaño y forma de despliegue dependen del contexto.

Starter con una fase de build real​

Descarga el starter DK2-06. No contiene solución Docker.

dk2-06-image-starter/
├── package.json
├── package-lock.json
├── README-starter.md
├── scripts/build.js
└── src/
├── server.js
└── response.js

Compruébalo fuera de Docker con Node.js 22:

npm ci
npm run build
npm start

npm ci instala esbuild, fijado como dependencia de desarrollo en el lockfile. npm run build ejecuta scripts/build.js: esbuild agrupa los dos módulos JavaScript en dist/server.js, preparado para Node.js 22. No hace falta aprender otro lenguaje ni un framework. El artefacto solo utiliza módulos propios de Node; no necesita esbuild ni node_modules para ejecutarse:

src + script + esbuild → dist/server.js → Node.js

Una primera imagen funcional​

Crea inicialmente:

FROM node:22-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY scripts ./scripts
COPY src ./src
RUN npm run build

ENV PORT=3000
EXPOSE 3000
CMD ["node", "dist/server.js"]

Y una .dockerignore mínima razonada:

.git
node_modules
dist
*.log
logs
.env

dist se excluye porque debe producirlo el build de la imagen, no copiar un artefacto posiblemente obsoleto del host. .env evita incorporar configuración local, pero no convierte Docker en un gestor de secretos. node_modules se instala dentro del build; .git y los logs no forman parte del artefacto. No excluyas package-lock.json: npm ci lo necesita.

Construye una referencia de comparación:

docker build -t dk206-app:antes .
docker image ls dk206-app
docker history dk206-app:antes

Ejecuta la imagen:

docker run -d --name dk206-antes -p 8086:3000 dk206-app:antes
docker logs dk206-antes

Abre http://localhost:8086/health; debe responder con status: "ok". Después libera el puerto para probar la siguiente imagen:

docker stop dk206-antes
docker rm dk206-antes

Revisar antes de optimizar​

Pregunta:

  • ¿qué base utilizamos y quién la mantiene?;
  • ¿qué archivos entraron en el contexto?;
  • ¿qué herramientas solo producen el artefacto?;
  • ¿qué necesita realmente dist/server.js?;
  • ¿con qué usuario se ejecuta?;
  • ¿qué rutas necesita escribir?;
  • ¿hay configuración sensible dentro de la imagen?

Medir primero permite distinguir una mejora de un cambio meramente diferente.

Imagen base y tags​

FROM node:22-alpine

es más controlado que node:latest, pero el tag sigue pudiendo actualizarse dentro de la misma línea. Fijar una versión no significa dejar de actualizar: significa hacerlo conscientemente, reconstruir y probar.

En la comparación mantendremos node:22-alpine tanto en la imagen inicial como en las dos etapas finales. Así observaremos la retirada de elementos de build sin mezclarla con un cambio de base. Construye ambas con la misma versión local de esa base.

Alpine no es siempre mejor. Puede reducir tamaño, pero también cambiar compatibilidad y herramientas disponibles. Una imagen mínima puede dificultar diagnóstico. Elige según requisitos, no por una regla automática.

Build frente a runtime​

La primera imagen conserva fuente, scripts, package files, node_modules y esbuild junto al artefacto, aunque al ejecutar solo necesitamos:

Node.js + dist/server.js
BUILD                         RUNTIME
src + scripts + herramientas → artefacto → Node + artefacto

No tiene sentido conservar herramientas de construcción si no participan durante la ejecución.

Dockerfile multi-stage​

Evoluciona a:

FROM node:22-alpine AS build

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY scripts ./scripts
COPY src ./src
RUN npm run build

FROM node:22-alpine AS runtime

WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000

COPY --from=build --chown=node:node /app/dist ./dist

USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

La etapa build produce el artefacto. La etapa final parte de una imagen limpia y recibe solo dist. No arrastra src, scripts, node_modules, esbuild ni los package files. No necesitamos instalar dependencias en runtime para este artefacto.

La imagen oficial de Node incluye el usuario y grupo node; COPY --chown=node:node entrega el artefacto con propietario coherente. Construye esta versión antes de comprobar usuario y funcionamiento:

docker build -t dk206-app:despues .

Usuario no root y permisos​

USER node aplica mínimo privilegio porque esta aplicación escucha en un puerto no privilegiado y no necesita modificar /app.

Si una aplicación necesita escribir, identifica una ruta concreta y prepara propietario y permisos. No uses:

chmod 777

como solución general. Amplía permisos indiscriminadamente y oculta el requisito real.

Comprueba el usuario efectivo:

docker run --rm dk206-app:despues id

Y que la aplicación funciona:

docker run -d --name dk206-app -p 8086:3000 dk206-app:despues
docker logs dk206-app

Abre http://localhost:8086/health y comprueba la misma respuesta que antes.

Comparar antes y después​

docker image ls dk206-app
docker history dk206-app:antes
docker history dk206-app:despues

Comprueba también el contenido:

docker run --rm dk206-app:antes ls -la /app
docker run --rm dk206-app:despues ls -la /app
docker run --rm dk206-app:despues sh -c "test ! -e /app/src && test ! -e /app/scripts && test ! -e /app/node_modules && test ! -e /app/package.json && test ! -e /app/package-lock.json && echo Runtime sin archivos de build"

Completa con evidencias:

CriterioAntesDespués
Funciona
Tamaño
Etapasunabuild + runtime
Usuario efectivo
Fuente en runtime
Scripts de build en runtime
esbuild y node_modules en runtime
Capacidad de diagnóstico

Menor tamaño no demuestra mayor seguridad ni mejor mantenimiento. Justifica el equilibrio.

Dockerfile y Compose tienen responsabilidades distintas​

Dockerfile
→ contenido, build, usuario y proceso por defecto

compose.yaml
→ servicios, redes, volúmenes y configuración runtime

No hornees APP_NAME, contraseñas o URLs específicas de un despliegue. Proporciónalas al ejecutar:

services:
app:
build: .
environment:
APP_NAME: TasteMatch Distribución

Proceso principal y parada​

CMD ["node", "dist/server.js"] ejecuta la aplicación como proceso principal. No uses tail -f /dev/null para mantener vivo un contenedor roto.

El starter atiende SIGINT y SIGTERM cerrando el servidor. Comprueba que docker stop termina limpiamente:

docker stop dk206-app
docker logs dk206-app
docker inspect dk206-app --format "{{.State.Status}} {{.State.ExitCode}}"
docker rm dk206-app

Los logs deben mostrar SIGTERM y Servidor cerrado.; el estado final debe ser exited con código 0. No necesitamos profundizar en PID 1.

Healthcheck: decisión de imagen o despliegue​

El endpoint /health permite reutilizar lo aprendido. Un healthcheck puede pertenecer a la imagen si expresa salud intrínseca, o a Compose si depende del despliegue. No repetimos aquí su sintaxis: revisa DK2-05 y justifica dónde vive.

Secretos fuera​

No copies .env reales, tokens, claves o contraseñas. Tampoco los fijes con ENV o argumentos de build: una imagen se inspecciona, almacena y distribuye.

Antes de publicar revisa contexto, historial, archivos finales y configuración. DK2-07 hará visible el ciclo de distribución.

Errores habituales​

  • elegir Alpine o distroless por reflejo;
  • asumir que la imagen más pequeña es automáticamente mejor o segura;
  • usar latest como única política;
  • mantener herramientas de build en runtime sin necesidad;
  • añadir multi-stage a un proyecto sin artefacto real;
  • cambiar a no root sin preparar propietarios y rutas;
  • solucionar permisos con chmod 777;
  • eliminar toda posibilidad razonable de diagnóstico;
  • copiar secretos o configuración local;
  • mezclar decisiones de build con configuración de despliegue.

Mini reto: auditoría antes/después​

  1. Ejecuta el starter fuera de Docker.
  2. Construye una imagen funcional de una etapa y mide tamaño, usuario y contenido.
  3. Revisa y justifica cada patrón de .dockerignore.
  4. Dibuja build y runtime.
  5. Implementa dos etapas sin copiar fuente ni scripts a runtime.
  6. Usa el usuario node y permisos mínimos.
  7. Comprueba que /health funciona y que la parada es limpia.
  8. Verifica que no hay secretos conocidos.
  9. Compara ambas imágenes con image ls e history.
  10. Explica qué ganaste y qué capacidad de diagnóstico conservaste.
  11. Ejecuta mediante Compose con configuración runtime.
  12. Decide razonadamente dónde definirías el healthcheck.

Una imagen más adecuada para distribución es el resultado de decisiones comprobadas, no de una plantilla universal.