Compartir comentarios
Las respuestas se generan en base a la documentación.

Compilaciones multiplataforma

Una compilación multiplataforma se refiere a una única invocación de compilación que tiene como objetivo múltiples combinaciones diferentes de sistema operativo o arquitectura de CPU. Al compilar imágenes, esto te permite crear una sola imagen que se puede ejecutar en múltiples plataformas, como linux/amd64, linux/arm64 y windows/amd64.

¿Por qué compilaciones multiplataforma?

Docker resuelve el problema de "funciona en mi máquina" empaquetando las aplicaciones y sus dependencias en contenedores. Esto facilita la ejecución de la misma aplicación en diferentes entornos, como desarrollo, pruebas y producción.

Pero la contenedorización por sí sola solo resuelve parte del problema. Los contenedores comparten el kernel del host, lo que significa que el código que se está ejecutando dentro del contenedor debe ser compatible con la arquitectura del host. Es por esto que no puedes ejecutar un contenedor linux/amd64 en un host arm64 (sin usar emulación), o un contenedor de Windows en un host Linux.

Las compilaciones multiplataforma resuelven este problema empaquetando múltiples variantes de la misma aplicación en una sola imagen. Esto te permite ejecutar la misma imagen en diferentes tipos de hardware, como máquinas de desarrollo que ejecutan x86-64 o instancias de Amazon EC2 basadas en ARM en la nube, sin necesidad de emulación.

Diferencia entre imágenes de una sola plataforma e imágenes multiplataforma

Las imágenes multiplataforma tienen una estructura diferente a las imágenes de una sola plataforma. Las imágenes de una sola plataforma contienen un único manifiesto que apunta a una única configuración y a un único conjunto de capas. Las imágenes multiplataforma contienen una lista de manifiestos, que apunta a múltiples manifiestos, cada uno de los cuales apunta a una configuración y un conjunto de capas diferentes.

Estructura de imagen multiplataforma

Cuando subes una imagen multiplataforma a un registro, el registro almacena la lista de manifiestos y todos los manifiestos individuales. Cuando descargas la imagen, el registro devuelve la lista de manifiestos y Docker selecciona automáticamente la variante correcta en función de la arquitectura del host. Por ejemplo, si ejecutas una imagen multiplataforma en una Raspberry Pi basada en ARM, Docker selecciona la variante linux/arm64. Si ejecutas la misma imagen en una laptop x86-64, Docker selecciona la variante linux/amd64 (si estás usando contenedores Linux).

Requisitos previos

Las imágenes multiplataforma requieren un almacenamiento de imágenes que admita listas de manifiestos. Docker Desktop y Docker Engine 29.0+ utilizan el almacenamiento de imágenes de containerd por defecto, el cual admite imágenes multiplataforma de forma nativa. Si estás utilizando una de estas versiones, no se necesita ninguna configuración adicional.

Si estás utilizando una versión anterior de Docker Engine, o si actualizaste desde una versión anterior que todavía utiliza los controladores de almacenamiento clásicos, tienes dos opciones:

  • Habilitar el almacenamiento de imágenes de containerd utilizando el archivo de configuración del demonio.
  • Crear un builder personalizado utilizando el controlador docker-container (consulta la sección siguiente).

Builder personalizado

Como alternativa al uso del almacenamiento de imágenes de containerd, puedes crear un builder personalizado que utilice el controlador docker-container. Este controlador admite compilaciones multiplataforma, pero las imágenes resultantes no se cargan en el almacenamiento de imágenes de tu Docker Engine. Puedes subirlas a un registro de contenedores directamente con docker build --push.

$ docker buildx create \
  --name container-builder \
  --driver docker-container \
  --bootstrap --use
Note

Las compilaciones con el controlador docker-container no se cargan automáticamente en el almacenamiento de imágenes de tu Docker Engine. Para obtener más información, consulta Controladores de compilación.

Si estás utilizando Docker Engine de forma independiente y necesitas compilar imágenes multiplataforma utilizando emulación, también necesitas instalar QEMU; consulta Instalar QEMU manualmente.

Compilar imágenes multiplataforma

Al iniciar una compilación, utiliza la opción --platform para definir las plataformas de destino para la salida de la compilación, como linux/amd64 y linux/arm64:

$ docker buildx build --platform linux/amd64,linux/arm64 .

Estrategias

Puedes compilar imágenes multiplataforma utilizando tres estrategias diferentes, según tu caso de uso:

  1. Usando emulación, a través de QEMU
  2. Usar un builder con múltiples nodos nativos
  3. Usar compilación cruzada con compilaciones multi-etapa

QEMU

Compilar imágenes multiplataforma bajo emulación con QEMU es la forma más fácil de comenzar si tu builder ya lo admite. El uso de la emulación no requiere cambios en tu Dockerfile, y BuildKit detecta automáticamente las arquitecturas que están disponibles para la emulación.

Note

La emulación con QEMU puede ser mucho más lenta que las compilaciones nativas, especialmente para tareas de procesamiento intensivo como la compilación y la compresión o descompresión.

En su lugar, utiliza múltiples nodos nativos o compilación cruzada, si es posible.

Docker Desktop admite la ejecución y compilación de imágenes multiplataforma bajo emulación por defecto. No se necesita ninguna configuración, ya que el builder utiliza el QEMU que viene integrado dentro de la máquina virtual de Docker Desktop.

Instalar QEMU manualmente

Si estás utilizando un builder fuera de Docker Desktop, como por ejemplo si estás utilizando Docker Engine en Linux, o un builder remoto personalizado, necesitas instalar QEMU y registrar los tipos de ejecutables en el sistema operativo del host. Los requisitos previos para instalar QEMU son:

  • Versión del kernel de Linux 4.8 o posterior
  • Versión de binfmt-support 2.1.7 o posterior
  • Los binarios de QEMU deben estar compilados estáticamente y registrados con la opción fix_binary

Utiliza la imagen tonistiigi/binfmt para instalar QEMU y registrar los tipos de ejecutables en el host con un solo comando:

$ docker run --privileged --rm tonistiigi/binfmt --install all

Esto instala los binarios de QEMU y los registra con binfmt_misc, permitiendo que QEMU ejecute formatos de archivo no nativos para la emulación.

Una vez que QEMU está instalado y los tipos de ejecutables están registrados en el sistema operativo del host, funcionan de manera transparente dentro de los contenedores. Puedes verificar tu registro comprobando si F se encuentra entre las banderas en /proc/sys/fs/binfmt_misc/qemu-*.

Múltiples nodos nativos

El uso de múltiples nodos nativos proporciona un mejor soporte para casos más complicados que QEMU no puede manejar, y también proporciona un mejor rendimiento.

Puedes añadir nodos adicionales a un builder utilizando la opción --append.

El siguiente comando crea un builder de múltiples nodos a partir de los contextos de Docker llamados node-amd64 y node-arm64. Este ejemplo asume que ya has añadido esos contextos.

$ docker buildx create --use --name mybuild node-amd64
mybuild
$ docker buildx create --append --name mybuild node-arm64
$ docker buildx build --platform linux/amd64,linux/arm64 .

Aunque este enfoque tiene ventajas sobre la emulación, la gestión de builders con múltiples nodos introduce cierta sobrecarga para configurar y administrar los clústeres de builders. Alternativamente, puedes usar Docker Build Cloud, un servicio que proporciona builders multi-nodo administrados en la infraestructura de Docker. Con Docker Build Cloud, obtienes builders nativos multiplataforma ARM y X86 sin la carga de tener que mantenerlos. El uso de builders en la nube también proporciona beneficios adicionales, como un caché de compilación compartido.

Después de registrarte en Docker Build Cloud, añade el builder a tu entorno local y comienza a compilar.

$ docker buildx create --driver cloud <ORG>/<BUILDER_NAME>
cloud-<ORG>-<BUILDER_NAME>
$ docker build \
  --builder cloud-<ORG>-<BUILDER_NAME> \
  --platform linux/amd64,linux/arm64,linux/arm/v7 \
  --tag <IMAGE_NAME> \
  --push .

Para obtener más información, consulta la documentación de Docker Build Cloud.

Compilación cruzada

Dependiendo de tu proyecto, si el lenguaje de programación que utilizas tiene un buen soporte para la compilación cruzada, puedes aprovechar las compilaciones multi-etapa para compilar binarios para las plataformas de destino desde la arquitectura nativa del builder. Los argumentos de compilación especiales, como BUILDPLATFORM y TARGETPLATFORM, están disponibles automáticamente para su uso en tu Dockerfile.

En el siguiente ejemplo, la instrucción FROM está anclada a la plataforma nativa del builder (utilizando la opción --platform=$BUILDPLATFORM) para evitar que se active la emulación. Luego, los argumentos de compilación predefinidos $BUILDPLATFORM y $TARGETPLATFORM se interpolan en una instrucción RUN. En este caso, los valores solo se muestran en la salida estándar con echo, pero esto ilustra cómo los pasarías al compilador para la compilación cruzada.

# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log
FROM alpine
COPY --from=build /log /log

Ejemplos

Aquí tienes algunos ejemplos de compilaciones multiplataforma:

Compilación multiplataforma simple usando emulación

Este ejemplo demuestra cómo compilar una imagen multiplataforma simple utilizando emulación con QEMU. La imagen contiene un único archivo que imprime la arquitectura del contenedor.

Requisitos previos:

Pasos:

  1. Crea un directorio vacío y accede a él:

    $ mkdir multi-platform
    $ cd multi-platform
    
  2. Crea un Dockerfile simple que imprima la arquitectura del contenedor:

    # syntax=docker/dockerfile:1
    FROM alpine
    RUN uname -m > /arch
  3. Compila la imagen para linux/amd64 y linux/arm64:

    $ docker build --platform linux/amd64,linux/arm64 -t multi-platform .
    
  4. Ejecuta la imagen e imprime la arquitectura:

    $ docker run --rm multi-platform cat /arch
    
    • Si estás ejecutando en una máquina x86-64, deberías ver x86_64.
    • Si estás ejecutando en una máquina ARM, deberías ver aarch64.

Compilación multiplataforma de Neovim usando Docker Build Cloud

Este ejemplo demuestra cómo ejecutar una compilación multiplataforma utilizando Docker Build Cloud para compilar y exportar los binarios de Neovim para las plataformas linux/amd64 y linux/arm64.

Docker Build Cloud proporciona builders multi-nodo administrados que admiten compilaciones multiplataforma nativas sin necesidad de emulación, lo que hace que sea mucho más rápido realizar tareas de uso intensivo de CPU como la compilación.

Requisitos previos:

Pasos:

  1. Crea un directorio vacío y accede a él:

    $ mkdir docker-build-neovim
    $ cd docker-build-neovim
    
  2. Crea un Dockerfile que compile Neovim.

    # syntax=docker/dockerfile:1
    FROM debian:bookworm AS build
    WORKDIR /work
    RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
        --mount=type=cache,target=/var/lib/apt,sharing=locked \
        apt-get update && apt-get install -y \
        build-essential \
        cmake \
        curl \
        gettext \
        ninja-build \
        unzip
    ADD https://github.com/neovim/neovim.git#stable .
    RUN make CMAKE_BUILD_TYPE=RelWithDebInfo
    
    FROM scratch
    COPY --from=build /work/build/bin/nvim /
  3. Compila la imagen para linux/amd64 y linux/arm64 utilizando Docker Build Cloud:

    $ docker build \
       --builder <cloud-builder> \
       --platform linux/amd64,linux/arm64 \
       --output ./bin .
    

    Este comando compila la imagen utilizando el builder en la nube y exporta los binarios al directorio bin.

  4. Verifica que los binarios estén compilados para ambas plataformas. Deberías ver el binario nvim tanto para linux/amd64 como para linux/arm64.

    $ tree ./bin
    ./bin
    ├── linux_amd64
    │   └── nvim
    └── linux_arm64
        └── nvim
    
    3 directories, 2 files
    

Compilación cruzada de una aplicación en Go

Este ejemplo demuestra cómo compilar de manera cruzada una aplicación en Go para múltiples plataformas utilizando compilaciones multi-etapa. La aplicación es un servidor HTTP simple que escucha en el puerto 8080 y devuelve la arquitectura del contenedor. Este ejemplo utiliza Go, pero los mismos principios se aplican a otros lenguajes de programación que admiten compilación cruzada.

La compilación cruzada con compilaciones de Docker funciona aprovechando una serie de argumentos de compilación predefinidos (en BuildKit) que te brindan información sobre las plataformas del builder y los destinos de la compilación. Puedes usar estos argumentos predefinidos para pasar la información de la plataforma al compilador.

En Go, puedes usar las variables de entorno GOOS y GOARCH para especificar la plataforma de destino para la cual compilar.

Requisitos previos:

  • Docker Desktop o Docker Engine

Pasos:

  1. Crea un directorio vacío y accede a él:

    $ mkdir go-server
    $ cd go-server
    
  2. Crea un Dockerfile base que compile la aplicación Go:

    # syntax=docker/dockerfile:1
    FROM golang:alpine AS build
    WORKDIR /app
    ADD https://github.com/dvdksn/buildme.git#eb6279e0ad8a10003718656c6867539bd9426ad8 .
    RUN go build -o server .
    
    FROM alpine
    COPY --from=build /app/server /server
    ENTRYPOINT ["/server"]

    Este Dockerfile aún no puede compilar en multiplataforma con compilación cruzada. Si intentaras compilar este Dockerfile con docker build, el builder intentaría usar emulación para compilar la imagen para las plataformas especificadas.

  3. Para añadir soporte de compilación cruzada, actualiza el Dockerfile para usar los argumentos de compilación predefinidos BUILDPLATFORM, TARGETOS y TARGETARCH.

    • Ancla la imagen golang a la plataforma del builder utilizando la opción --platform=$BUILDPLATFORM.
    • Añade instrucciones ARG para las etapas de compilación de Go para hacer que los argumentos de compilación TARGETOS y TARGETARCH estén disponibles para los comandos en esta etapa.
    • Establece las variables de entorno GOOS y GOARCH con los valores de TARGETOS y TARGETARCH. El compilador de Go utiliza estas variables para realizar la compilación cruzada.
    # syntax=docker/dockerfile:1
    FROM --platform=$BUILDPLATFORM golang:alpine AS build
    ARG TARGETOS
    ARG TARGETARCH
    WORKDIR /app
    ADD https://github.com/dvdksn/buildme.git#eb6279e0ad8a10003718656c6867539bd9426ad8 .
    RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o server .
     
    FROM alpine
    COPY --from=build /app/server /server
    ENTRYPOINT ["/server"]
    # syntax=docker/dockerfile:1
    FROM golang:alpine AS build
    WORKDIR /app
    ADD https://github.com/dvdksn/buildme.git#eb6279e0ad8a10003718656c6867539bd9426ad8 .
    RUN go build -o server .
     
    FROM alpine
    COPY --from=build /app/server /server
    ENTRYPOINT ["/server"]
    # syntax=docker/dockerfile:1
    -FROM golang:alpine AS build
    +FROM --platform=$BUILDPLATFORM golang:alpine AS build
    +ARG TARGETOS
    +ARG TARGETARCH
    WORKDIR /app
    ADD https://github.com/dvdksn/buildme.git#eb6279e0ad8a10003718656c6867539bd9426ad8 .
    -RUN go build -o server .
    +RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o server .
     
    FROM alpine
    COPY --from=build /app/server /server
    ENTRYPOINT ["/server"]
    
  4. Compila la imagen para linux/amd64 y linux/arm64:

    $ docker build --platform linux/amd64,linux/arm64 -t go-server .
    

Este ejemplo ha mostrado cómo realizar la compilación cruzada de una aplicación en Go para múltiples plataformas con compilaciones de Docker. Los pasos específicos sobre cómo realizar la compilación cruzada pueden variar según el lenguaje de programación que estés utilizando. Consulta la documentación de tu lenguaje de programación para obtener más información sobre la compilación cruzada para diferentes plataformas.

Tip

También podrías considerar echar un vistazo a xx - Ayudantes de compilación cruzada para Dockerfile. xx es una imagen de Docker que contiene scripts de utilidad que facilitan la compilación cruzada con compilaciones de Docker.