Docker Buildx é como aquele mecânico especialista que você chama quando quer tunar seu carro para correr mais rápido e ser mais eficiente.
Ele é uma ferramenta relativamente nova, mas incrivelmente poderosa, que resolve várias dores de cabeça das builds tradicionais no Docker.
Neste artigo, vamos explorar o que é o Docker Buildx, como ele funciona por debaixo dos panos e por que ele é tão eficiente para construir imagens. Para deixar ainda mais claro, faremos uma comparação prática com um exemplo de uma aplicação em Go e Java 😅.
Como o Docker Buildx funciona?
O Docker Buildx é uma interface poderosa que estende o BuildKit, o mecanismo de build mais moderno do Docker. O BuildKit já é responsável por otimizar o processo de construção de imagens, mas o Buildx eleva essa eficiência expondo funcionalidades que a CLI tradicional do Docker não oferece. O Buildx se conecta ao BuildKit para habilitar recursos como caching inteligente, suporte multiplataforma, paralelismo e cache distribuído.
No centro de tudo está o LLB (Low-Level Build), um formato intermediário binário que define o grafo de dependências para cada operação de build. O LLB permite que o BuildKit rastreie precisamente quais camadas ou operações foram modificadas e quais podem ser reutilizadas do cache, tornando o processo de build mais rápido, eficiente e preciso.
Caching Inteligente
O BuildKit, através do LLB, tem uma maneira muito mais sofisticada de lidar com cache comparado ao Docker tradicional. O Buildx usa essa capacidade para permitir caching entre diferentes pipelines e máquinas. Toda a execução e caching de builds são definidos no LLB, o que permite ao BuildKit rastrear checksums de grafos de dependências e conteúdos montados. Isso significa que, se uma build já foi feita anteriormente, o cache pode ser utilizado para evitar recompilar partes que não mudaram. Além disso, o cache pode ser exportado para um registro (Docker Hub ou um registro privado), sendo reutilizado em diferentes ambientes de build.
Por exemplo, se você construir uma imagem em um servidor de CI/CD com cache habilitado, ao rodar a mesma build em outro servidor ou pipeline, o Buildx reutiliza esse cache e reconstrói apenas o que foi alterado, economizando um tempo considerável.
Multi-plataforma
O Buildx também é excepcional quando falamos de builds multiplataforma. Através do LLB, o BuildKit é capaz de compilar para diferentes arquiteturas, como amd64 e arm64, de forma simultânea. Isso elimina a necessidade de usar hacks ou ferramentas de emulação (como QEMU) para criar imagens compatíveis com diferentes tipos de hardware. O LLB define essas arquiteturas como dependências dentro do grafo de build, e o BuildKit gerencia automaticamente a execução dessas operações.
Com isso, um único comando de build pode gerar imagens que rodem tanto em um servidor com arquitetura x86 quanto em dispositivos ARM, como o Raspberry Pi ou os novos Macs com chip M1/M2. Esse recurso facilita a vida de desenvolvedores que precisam garantir compatibilidade em ambientes heterogêneos sem necessidade de manter múltiplos scripts ou configurações complexas.
Paralelismo
Outra vantagem do Buildx, habilitada pelo LLB, é o paralelismo durante a build. Diferentes partes do Dockerfile, ou diferentes camadas, podem ser processadas ao mesmo tempo. O LLB permite que o BuildKit crie um grafo de operações, identificando quais etapas podem ser executadas em paralelo, aproveitando melhor o hardware disponível. Isso elimina a abordagem sequencial das builds tradicionais, onde cada etapa espera pela anterior para ser finalizada.
Esse paralelismo torna o processo de build significativamente mais rápido, especialmente em Dockerfiles complexos que incluem várias etapas de compilação, instalação de dependências e configuração de ambiente.
Cache Distribuído
O cache distribuído é outro ponto forte do Buildx, facilitado pelo LLB. O BuildKit permite salvar o cache de build em locais distribuídos, como serviços de armazenamento em nuvem (Amazon S3, Google Cloud, etc.). Isso é essencial em ambientes de CI/CD, onde o cache pode ser compartilhado entre diferentes máquinas e pipelines, acelerando os tempos de build em ambientes diversos.
Por exemplo, se uma equipe de desenvolvimento está rodando builds em várias máquinas ou servidores, o BuildKit pode sincronizar o cache entre esses sistemas, garantindo que o tempo de build seja reduzido ao máximo, reutilizando o cache previamente gerado em um local centralizado.
Otimização com LLB
O uso do LLB no BuildKit redefine completamente o modelo de cache e execução de builds. O LLB rastreia diretamente os checksums de grafos de build e do conteúdo montado para operações específicas. Isso permite que o Buildx identifique com precisão o que precisa ser reconstruído em uma nova build, evitando recompilar partes que já estão prontas e armazenadas no cache.
Se, por exemplo, apenas o código da aplicação foi alterado, mas as dependências instaladas permanecem as mesmas, o Buildx utiliza o cache para essas dependências e recompila apenas a parte do código alterada. Essa precisão traz uma economia significativa de tempo e recursos computacionais, otimizando ainda mais o processo de build.
Mão na massa
Agora que entendemos a teoria, vamos colocar a mão na massa e ver como isso se aplica em um caso prático. Digamos que temos uma aplicação simples em Go e outra em Java que queremos construir e testar em diferentes arquiteturas (como x86 e ARM).Vamos ver como o Buildx vai te salvar nessa missão!
Lembrando que todos os códigos mostrados a baixo estão disponíveis no meu github :)
Build de uma API em Go
Vamos começar com uma API REST em Go. A API vai listar usuários (nada muito complexo, mas serve para ilustrar o poder do Buildx).
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func usersHandler(c echo.Context) error {
users := []User{
{ID: 1, Name: "John Doe", Email: "john@example.com"},
{ID: 2, Name: "Jane Doe", Email: "jane@example.com"},
}
return c.JSON(http.StatusOK, users)
}
func main() {
e := echo.New()
e.GET("/users", usersHandler)
e.Logger.Fatal(e.Start(":8080"))
}
Como temos o intuido de testar o docker buildx, hora de escrever o Dockerfile
:
# syntax=docker/dockerfile:1
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o users-api ./cmd/service
FROM scratch
WORKDIR /app
COPY --from=builder /app/users-api .
EXPOSE 8080
CMD ["./users-api"]
Vamos deixar esses exemplos de lado por um tempinho e criar a API Java, no final vamos comparar as performances de build normal e buildx :)
Build de uma API em Java (Micronaut + Java 21)
Agora vamos para o Java! Usando Micronaut (porque ninguém merece esperar a inicialização do Spring em projetos pequenos), vamos criar uma API que lista produtos.
package com.rflpazini.handler;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.serde.annotation.Serdeable;
import java.util.List;
@Controller("/products")
class Products {
@Get("/")
public List<Product> listProducts() {
return List.of(
new Product(1, "Laptop", 999.99),
new Product(2, "Smartphone", 699.99)
);
}
}
@Serdeable
record Product(int id, String name, double price) {}
E agora, o Dockerfile
# syntax=docker/dockerfile:1
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY . .
RUN ./gradlew build --no-daemon
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar /app/application.jar
EXPOSE 8080
CMD ["java", "-jar", "application.jar"]
Comparação de performance: Docker Build Normal vs Docker Buildx
Agora que construímos as aplicações em Go e Java usando o Docker Buildx, vamos comparar a performance das builds com o Docker tradicional e o Buildx. A diferença principal é que o Docker tradicional faz a build sequencial e para uma única arquitetura, enquanto o Docker Buildx pode fazer builds paralelas e multiplataforma.
Setup de Teste
Para realizar uma comparação justa, faremos a build nas seguintes condições:
- Docker build tradicional: Compilação para apenas uma arquitetura (linux/amd64).
- Docker Buildx: Compilação para múltiplas arquiteturas (linux/amd64, linux/arm64).
Build Normal (Docker Tradicional) - Go API
Para a build tradicional em Go, rodamos o comando:
time docker build -t users-api:latest .
E temos o seguinte resultado após o build:
Tempo de Build:
Arquitetura: arm64
Tempo total: ~19 segundos
Aqui estamos usando o Docker CLI tradicional, e ele faz a build apenas para arm64.
Build com Docker Buildx (Multiplataforma)
Agora, vamos fazer a mesma build com o Docker Buildx, usando multiplataforma (amd64 e arm64):
time docker buildx build --platform linux/amd64,linux/arm64 -t users-api:latest .
E o resultado do build:
Tempo de Build:
Arquiteturas: amd64 e arm64
Tempo total: ~28 segundos
Comparação de Performance (Go):
- Docker Tradicional: 19 segundos para amd64.
- Docker Buildx: 28 segundos para amd64 e arm64.
Build Normal (Docker Tradicional) - Java API
Rodando a build normal em Java, com o Docker CLI, vamos rodar os mesmos comandos:
time docker build -t product-api:latest .
tempo do build:
Tempo de Build:
Arquitetura: arm64
Tempo total: ~72 segundos
Aqui, o build Java tende a ser mais demorado por conta da fase de compilação e do tamanho do ambiente Java. Lembrem que não estamos comparando linguagens, apenas os builds de cada API. :D
Build com Docker Buildx (Multiplataforma)
Agora, vamos rodar a build usando o Docker Buildx para multiplataforma:
time docker buildx build --platform linux/amd64,linux/arm64 -t product-api:latest .
Que rufem os tambores... Tempo de build com o Buildx:
Tempo de Build:
Arquitetura: amd64
Tempo total: ~112 segundos
Comparação de Performance (Java):
- Docker Tradicional: 72 segundos para arm64.
- Docker Buildx: 112 segundos para amd64 e arm64.
O Buildx leva um pouquinho mais de tempo porque está gerando imagens para duas arquiteturas, mas ainda assim o ganho de flexibilidade vale a pena. Sem o Buildx, seria necessário rodar duas builds separadas, uma para cada arquitetura, o que tornaria o processo bem mais lento. Podemos rodar o Buildx apenas em uma arquitetura também, o que poderá diminuir os valores em relação ao build normal
Conclusão Geral
É meus amigos... Terminou! E vamos para as considerações finais:
- Docker Tradicional: Faz uma build para uma única arquitetura, o que pode ser rápido, mas não escalável quando você precisa distribuir suas aplicações em diferentes plataformas. Ele também não tem caching inteligente ou paralelismo, o que aumenta o tempo em builds maiores.
- Docker Buildx: Faz builds multiplataforma de forma eficiente (pode fazer de apenas uma arquitetura também), gerando imagens tanto para amd64 quanto para arm64 com um único comando. O tempo de build é um pouco maior, mas a flexibilidade e o suporte para diferentes arquiteturas compensam muito, especialmente em ambientes heterogêneos. Além disso, com caching inteligente e paralelismo, o Buildx tende a ser mais eficiente em builds complexas.
No mundo real, onde você precisa garantir que suas imagens rodem em diferentes tipos de hardware, o Docker Buildx faz toda a diferença e otimiza o processo de deploy, te poupando tempo e dores de cabeça! 🚀
Top comments (0)