Compilaciones multi-etapa
Las compilaciones multi-etapa (multi-stage builds) son útiles para cualquiera que haya tenido dificultades para optimizar sus Dockerfiles manteniéndolos al mismo tiempo fáciles de leer y mantener.
Usar compilaciones multi-etapa
Con las compilaciones multi-etapa, utilizas múltiples instrucciones FROM en tu Dockerfile.
Cada instrucción FROM puede usar una base diferente, y cada una de ellas comienza una nueva
etapa de la compilación. Puedes copiar selectivamente artefactos de una etapa a
otra, dejando atrás todo lo que no quieras en la imagen final.
El siguiente Dockerfile tiene dos etapas separadas: una para compilar un binario y otra donde el binario se copia de la primera etapa a la siguiente.
# syntax=docker/dockerfile:1
FROM golang:1.25
WORKDIR /src
COPY <<EOF ./main.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go
FROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]Solo necesitas un único Dockerfile. No es necesario un script de compilación separado. Simplemente
ejecuta docker build.
$ docker build -t hello .
El resultado final es una imagen de producción diminuta que no contiene más que el binario en su interior. Ninguna de las herramientas de compilación requeridas para compilar la aplicación se incluye en la imagen resultante.
¿Cómo funciona? La segunda instrucción FROM comienza una nueva etapa de compilación con
la imagen scratch como base. La línea COPY --from=0 copia únicamente el
artefacto compilado de la etapa anterior a esta nueva etapa. El SDK de Go y cualquier
artefacto intermedio se dejan atrás y no se guardan en la imagen final.
Nombrar tus etapas de compilación
Por defecto, las etapas no tienen nombre y te refieres a ellas por su número entero,
comenzando con el 0 para la primera instrucción FROM. Sin embargo, puedes
nombrar tus etapas añadiendo un AS <NOMBRE> a la instrucción FROM. Este
ejemplo mejora el anterior al nombrar las etapas y usar el nombre en la
instrucción COPY. Esto significa que incluso si las instrucciones en tu
Dockerfile se reordenan más adelante, el comando COPY no se romperá.
# syntax=docker/dockerfile:1
FROM golang:1.25 AS build
WORKDIR /src
COPY <<EOF /src/main.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go
FROM scratch
COPY --from=build /bin/hello /bin/hello
CMD ["/bin/hello"]Detenerse en una etapa de compilación específica
Cuando compilas tu imagen, no necesitas necesariamente compilar todo el
Dockerfile incluyendo cada etapa. Puedes especificar una etapa de compilación de destino. El
siguiente comando asume que estás utilizando el Dockerfile anterior pero se detiene en
la etapa llamada build:
$ docker build --target build -t hello .
Algunos escenarios donde esto podría ser útil son:
- Depurar una etapa de compilación específica
- Usar una etapa de
debugcon todos los símbolos o herramientas de depuración habilitados, y una etapa deproductionmás ligera - Usar una etapa de
testingen la que tu aplicación se llene con datos de prueba, pero compilando para producción usando una etapa diferente que utiliza datos reales
Usar una imagen externa como etapa
Al utilizar compilaciones multi-etapa, no estás limitado a copiar de las etapas que
creaste previamente en tu Dockerfile. Puedes usar la instrucción COPY --from para
copiar desde una imagen separada, ya sea utilizando el nombre de la imagen local, una etiqueta disponible
localmente o en un registro de Docker, o un ID de etiqueta. El cliente de Docker descargará la imagen
si es necesario y copiará el artefacto desde allí. La sintaxis es:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.confUsar una etapa anterior como una nueva etapa
Puedes retomar el trabajo donde lo dejó una etapa anterior haciendo referencia a ella al utilizar
la directiva FROM. Por ejemplo:
# syntax=docker/dockerfile:1
FROM alpine:latest AS builder
RUN apk --no-cache add build-base
FROM builder AS build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp
FROM builder AS build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cppDiferencias entre el builder heredado y BuildKit
El builder heredado de Docker Engine procesa todas las etapas de un Dockerfile que conducen
al --target seleccionado. Compilará una etapa incluso si el destino
seleccionado no depende de esa etapa.
BuildKit solo compila las etapas de las que depende la etapa de destino.
Por ejemplo, dado el siguiente Dockerfile:
# syntax=docker/dockerfile:1
FROM ubuntu AS base
RUN echo "base"
FROM base AS stage1
RUN echo "stage1"
FROM base AS stage2
RUN echo "stage2"Con BuildKit habilitado, compilar el
destino stage2 en este Dockerfile significa que solo se procesan base y stage2.
No hay dependencia de stage1, por lo que se omite.
$ DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .
[+] Building 0.4s (7/7) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 36B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:latest 0.0s
=> CACHED [base 1/2] FROM docker.io/library/ubuntu 0.0s
=> [base 2/2] RUN echo "base" 0.1s
=> [stage2 1/1] RUN echo "stage2" 0.2s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:f55003b607cef37614f607f0728e6fd4d113a4bf7ef12210da338c716f2cfd15 0.0s
Por otro lado, compilar el mismo destino sin BuildKit da como resultado que se procesen todas las etapas:
$ DOCKER_BUILDKIT=0 docker build --no-cache -f Dockerfile --target stage2 .
Sending build context to Docker daemon 219.1kB
Step 1/6 : FROM ubuntu AS base
---> a7870fd478f4
Step 2/6 : RUN echo "base"
---> Running in e850d0e42eca
base
Removing intermediate container e850d0e42eca
---> d9f69f23cac8
Step 3/6 : FROM base AS stage1
---> d9f69f23cac8
Step 4/6 : RUN echo "stage1"
---> Running in 758ba6c1a9a3
stage1
Removing intermediate container 758ba6c1a9a3
---> 396baa55b8c3
Step 5/6 : FROM base AS stage2
---> d9f69f23cac8
Step 6/6 : RUN echo "stage2"
---> Running in bbc025b93175
stage2
Removing intermediate container bbc025b93175
---> 09fc3770a9c4
Successfully built 09fc3770a9c4
El builder heredado procesa stage1, incluso si stage2 no depende de él.