Multi-stage build

Clase 31 de 33Curso de Docker

Contenido del curso

Resumen

Construir imágenes Docker que solo contengan lo estrictamente necesario para producción es una práctica fundamental en cualquier proyecto profesional. La técnica de multi-stage builds permite separar las fases de compilación, testing y producción dentro de un mismo Dockerfile, logrando imágenes finales más livianas, seguras y rápidas de transferir.

¿Por qué no debería incluir todo el código en la imagen productiva?

En proyectos reales, especialmente con lenguajes compilados que transforman código fuente en binarios ejecutables, no tiene sentido cargar todo el código original en la imagen final. Lo mismo ocurre con el código de testing: queremos ejecutar los tests en cada build, pero una vez que pasan, ese código no necesita estar presente en producción [01:00].

La alternativa ingenua de copiar todo, ejecutar lo necesario y luego borrar archivos genera un layer adicional que no resuelve el problema. Esa capa extra invalida parte del caché anterior y el código nunca debería haber estado ahí en primer lugar [01:38].

¿Cómo funcionan los multi-stage builds en Docker?

Docker introdujo los multi-stage builds como solución elegante a este problema. La idea central es utilizar múltiples instrucciones FROM dentro de un mismo Dockerfile, donde cada FROM define un stage o fase del proceso de construcción [03:15].

¿Qué papel cumple cada stage en el Dockerfile?

En un proyecto típico encontrarás dos Dockerfiles separados: uno para desarrollo y otro para producción [02:36].

  • El development Dockerfile incluye herramientas como nodemon para monitorear cambios en archivos durante el desarrollo.
  • El production Dockerfile usa multi-stage builds con dos fases diferenciadas.

La primera fase se nombra con la sintaxis AS builder [03:42]. Dentro de ella se realizan estas operaciones:

  • Se copian los archivos necesarios para instalar dependencias.
  • Se ejecuta npm install --only=production para instalar solo dependencias productivas.
  • Se copian todos los archivos del proyecto.
  • Se instalan las dependencias de desarrollo (devDependencies) como Mocha para testing.
  • Se ejecutan los tests con npm run test [04:28].

La segunda fase, que genera la imagen final, usa COPY --from=builder para traer únicamente lo que necesita de la fase anterior [03:30]. Solo copia las dependencias productivas y el archivo index.js, sin tests ni herramientas de desarrollo.

¿Por qué separar la instalación de dependencias productivas y de desarrollo?

Esta separación no es capricho. Las primeras líneas de ambas fases son idénticas: misma imagen base, mismos archivos copiados, mismo npm install --only=production [05:17]. Gracias a esto, Docker reutiliza las capas del caché de la primera fase cuando construye la segunda.

En la práctica, la fase final solo necesita procesar la copia de un archivo, el EXPOSE y el CMD, operaciones que toman tiempo prácticamente nulo [06:20].

¿Cómo se construye y verifica una imagen multi-stage?

Para construir la imagen se usa el flag -f que permite especificar qué Dockerfile utilizar cuando hay más de uno en el proyecto [06:03]:

bash docker build -t prodapp -f build/production.dockerfile .

Durante el build se observan todos los pasos de ambas fases. Los tests se ejecutan en la primera fase y, si pasan, la construcción continúa hacia la imagen final [06:40]. Al inspeccionar el resultado con docker image ls, la imagen prodapp tiene un tamaño reducido.

Al entrar al contenedor con docker exec -it prod bash y listar archivos, solo aparece el index.js y los módulos de Node productivos [07:30]. No hay tests ni herramientas de desarrollo.

¿Cómo funciona como herramienta de integración continua?

Un beneficio adicional es que el multi-stage build actúa como un script de integración continua básico. Si algún test falla, el build se detiene completamente y no genera la imagen [08:00]. Esto garantiza que nunca se produzca una imagen con código que no pasa las pruebas.

Para comprobarlo, basta con modificar un test para que falle: el layer se invalida, el test corre de nuevo, falla y el build se interrumpe con un error [08:28].

Esta técnica es omnipresente en proyectos profesionales. Permite mantener un solo comando docker build que resuelve todo el flujo, aprovechando el caché de capas y produciendo imágenes que contienen exactamente lo necesario para ejecutarse en producción.