# 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](/build/images/single-vs-multiplatform-image.svg)

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](/desktop/features/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](/engine/storage/containerd/).
- 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`.

```console
$ 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](/build/builders/drivers/).

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](#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`:

```console
$ 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](#qemu)
2. Usar un builder con [múltiples nodos nativos](#múltiples-nodos-nativos)
3. Usar [compilación cruzada](#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](#múltiples-nodos-nativos) o
> [compilación cruzada](#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`](https://github.com/tonistiigi/binfmt) para
instalar QEMU y registrar los tipos de ejecutables en el host con un solo
comando:

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

Esto instala los binarios de QEMU y los registra con
[`binfmt_misc`](https://en.wikipedia.org/wiki/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.

```console
$ 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.

```console
$ 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](/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.

```dockerfile
# 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](#compilación-multiplataforma-simple-usando-emulación)
- [Compilación multiplataforma de Neovim usando Docker Build Cloud](#compilación-multiplataforma-de-neovim-usando-docker-build-cloud)
- [Compilación cruzada de una aplicación en Go](#compilación-cruzada-de-una-aplicación-en-go)

### 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:

- Docker Desktop, o Docker Engine con [QEMU instalado](#instalar-qemu-manually)

Pasos:

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

   ```console
   $ mkdir multi-platform
   $ cd multi-platform
   ```

2. Crea un Dockerfile simple que imprima la arquitectura del contenedor:

   ```dockerfile
   # syntax=docker/dockerfile:1
   FROM alpine
   RUN uname -m > /arch
   ```

3. Compila la imagen para `linux/amd64` y `linux/arm64`:

   ```console
   $ docker build --platform linux/amd64,linux/arm64 -t multi-platform .
   ```

4. Ejecuta la imagen e imprime la arquitectura:

   ```console
   $ 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](https://github.com/neovim/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:

- Te has [registrado en Docker Build Cloud y has creado un builder](/build-cloud/setup/)

Pasos:

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

   ```console
   $ mkdir docker-build-neovim
   $ cd docker-build-neovim
   ```

2. Crea un Dockerfile que compile Neovim.

   ```dockerfile
   # 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:

   ```console
   $ 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`.

   ```console
   $ 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:

   ```console
   $ mkdir go-server
   $ cd go-server
   ```

2. Crea un Dockerfile base que compile la aplicación Go:

   ```dockerfile
   # 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.

   **Dockerfile actualizado**



   ```dockerfile
   # 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"]
   ```

   **Dockerfile antiguo**



   ```dockerfile
   # 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"]
   ```

   **Diff**



   ```diff
   # 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`:

   ```console
   $ 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](https://github.com/tonistiigi/xx).
> `xx` es una imagen de Docker que contiene scripts de utilidad que facilitan la compilación cruzada con compilaciones de Docker.


