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:
| Criterio | Antes | Después |
|---|---|---|
| Funciona | ||
| Tamaño | ||
| Etapas | una | build + 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
latestcomo ú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
- Ejecuta el starter fuera de Docker.
- Construye una imagen funcional de una etapa y mide tamaño, usuario y contenido.
- Revisa y justifica cada patrón de
.dockerignore. - Dibuja build y runtime.
- Implementa dos etapas sin copiar fuente ni scripts a runtime.
- Usa el usuario
nodey permisos mínimos. - Comprueba que
/healthfunciona y que la parada es limpia. - Verifica que no hay secretos conocidos.
- Compara ambas imágenes con
image lsehistory. - Explica qué ganaste y qué capacidad de diagnóstico conservaste.
- Ejecuta mediante Compose con configuración runtime.
- 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.