Intro
Las multistage build son compilaciones de imágenes de contenedores en varias fases. Esto nos permite usar otros contenedores para generar los ficheros o binarios necesarios para el contenedor de la siguiente fase, y así reducir el tamaño de la imagen final.
Una de las formas de crear imágenes en varias fases, es dar un nombre a la fase en la línea FROM desde la que importamos la imágen; y luego referenciar dicha fase con el comando COPY para copiar los ficheros generados en la fase anterior:
Por ejemplo:
#Fase builder
FROM node:latest AS BUILDER
...
#Fase RUNNER
FROM node:latest AS RUNNER
...
WORKDIR ./app
COPY --from=BUILDER ./build ./
Multistage build con Typescript
Para ilustrar un caso de uso real, vamos a utilizar una aplicación sencilla escrita en Typescript con Express. Como se está usando Typescript, existe una fase previa de compilación del código typescript a javascript. Ademas, vamos a generar la imagen final con los módulos de node estrictamente necesarios para la aplicación, dejando fuera los ficheros @types y el compilador de typescript. Así tendremos la imagen final lo mas pequeña posible.
La aplicación escuchara por el puerto 3333 a peticiones GET y devolverá la cadena “Hello world” a todas las peticiones. Aquí dejo el enlace a github para ver la aplicación al detalle.
Vamos a por el Dockerfile:
FROM node:latest as BUILDER
WORKDIR /app
COPY ./app ./
RUN npm install
RUN npm run compile
RUN rm -rf node_modules
FROM node:latest as FINAL
WORKDIR /app
COPY --from=BUILDER ./app/build/index.js ./app/package.json ./
RUN npm install --prod
CMD ["node", "index.js"]
Por supuesto los nombres dados a los contenedores en cada fase pueden ser los que queramos.
Fase BUILDER
En la primera fase, el BUILDER, se hacen los siguientes pasos:
- Cargamos la imagen base node:latest con FROM y establecemos el nombre de la fase con AS BUILDER
- Establecemos el directorio de trabajo del contenedor con WORKDIR hacia /app
- Copiar toda la aplicación de la máquina local hacia el contenedor con COPY
- Instalar todos los módulos de node declarados en package.json
- Lanzar la compilación. (Hay un script declarado en package.json que llama al compilador)
- Borrar los node_modules generados en este contenedor, ya que contienen las dependencias de desarrollo.
Fase FINAL
Esta es la fase final que contendrá todos los ficheros del contenedor.
- Cargar imagen base con FROM node:latest AS FINAL
- De nuevo establecer el directorio del contenedor con WORKDIR en /app
- Copiar los ficheros necesarios para la aplicación: package.json y la aplicación compilada
- Instalar las dependencias de producción.
- Establecer el comando por defecto del contenedor.
Build y run
Ahora que hemos definido los pasos queda construir el contenedor y lanzarlo.
docker build -t ts_multistage:v1 .
docker run -p 3333:3332 ts_multistage:v1
Para probarlo hacemos una petición http con curl
$ curl -v localhost:3333
* Trying 127.0.0.1:3333...
* Connected to localhost (127.0.0.1) port 3333 (#0)
> GET / HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.79.1
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: text/html; charset=utf-8
< Content-Length: 11
< ETag: W/"b-e1AsOh9IyGCa4hLN+2Od7jlnP14"
< Date: Mon, 08 Nov 2021 01:10:47 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host localhost left intact
Hello world%
Bola extra: Multistage builds en c++
También se puede utilizar las multistage para aplicaciones de c++. Una utilidad practica que le veo a compilar usando contenedores, es compilar el código fuente con nuevas versiones de gcc, sin tener que esperar que los nuevos compiladores lleguen a nuestra distribución.
En este ejemplo, estamos construyendo un contenedor cuya misión es compilar el código fuente (Fase Builder) y un último contenedor que ejecutará los binarios (FASE FINAL)
Tan solo necesitamos tres ficheros: main.cpp, Makefile y un Dockerfile
main.cpp
#include <iostream>
using namespace std;
int main(void) {
cout << "Hello world" << endl;
return 0;
}
Makefile
CC=g++
CPPFLAGS=-g -Wall -std=c++11
main.out: main.cc
${CC} ${CPPFLAGS} $^ -o $@
clean:
rm -f *.o *.out
Dockerfile
FROM gcc:latest as BUILDER
WORKDIR /build
COPY main.cc Makefile ./
RUN apt-get update && \
apt-get install make
RUN make
FROM ubuntu:latest AS FINAL
WORKDIR /app
COPY --from=BUILDER /build/main.out /app/
CMD ["./main.out"]
Links
https://docs.docker.com/develop/develop-images/multistage-build/