Build ARM64: 40 min → 8-12 min — eliminando QEMU no GitHub Actions
Publicado em 14 de abril de 2026

O problema: QEMU silencioso no runner x86
Um pipeline de CI/CD no GitHub Actions fazia o build da imagem Docker para um cluster OKE (Oracle Kubernetes Engine) com nodes ARM64 (instâncias OCI A1.Flex Ampere). O runner configurado era self-hosted em máquina x86. O Dockerfile era padrão, o buildx estava configurado corretamente — e o build levava ~40 minutos.
A causa não estava no Dockerfile nem no tamanho da aplicação (Next.js com 350 páginas geradas). Estava em uma linha do workflow:
- uses: docker/setup-qemu-action@v3
- run: docker buildx build --platform linux/arm64 .Quando um runner x86 encontra --platform linux/arm64, o Docker usa QEMU para emular a arquitetura ARM64 em hardware x86. A penalidade de emulação é de 5-10x no tempo de compilação — cada instrução ARM64 é traduzida em tempo real pelo emulador.
O setup-qemu-action não avisa que o build vai ser lento. Ele simplesmente habilita a emulação e o buildx segue. O resultado é um build funcionalmente correto, mas ordens de magnitude mais devagar do que seria em hardware nativo.
O contexto: migração AWS (x86) → OCI ARM64
O projeto havia migrado de AWS ECS (instâncias x86) para OKE com nodes A1.Flex (ARM64 Ampere) para redução de custos. A imagem de produção que rodava em ECS era amd64 — o primeiro build ARM64 foi feito manualmente com Docker buildx no Mac Apple Silicon (que é ARM nativo). Para CI/CD automatizado, o runner self-hosted existente (x86) foi reaproveitado com QEMU para evitar configurar nova infraestrutura.
Durante semanas, deploys de produção levavam ~40 minutos só na etapa de build. O restante do pipeline (push para OCIR, kubectl rollout) era questão de segundos.
A solução: ubuntu-24.04-arm no GitHub Actions
O GitHub oferece runners ARM64 gerenciados desde 2024. O runner ubuntu-24.04-arm executa em hardware ARM real — sem emulação, sem QEMU, sem penalidade de tradução de instruções.
O workflow foi dividido em dois jobs independentes:
# Antes — um único job x86 com QEMU para prod e stage
jobs:
Build-and-Deploy:
runs-on: self-hosted # x86
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3 # emulação ARM64
- uses: docker/setup-buildx-action@v3
- run: |
docker buildx build \
--platform linux/arm64 \ # força ARM64 via QEMU
--push \
-t $IMAGE_TAG .# Depois — jobs separados por branch/ambiente
jobs:
Build-and-Deploy-Stage:
runs-on: self-hosted # x86 — stage usa amd64, sem problema
if: github.ref == 'refs/heads/staging'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- run: docker buildx build --push -t $IMAGE_TAG .
Build-and-Deploy-Prod:
runs-on: ubuntu-24.04-arm # ARM nativo — sem QEMU
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- run: |
docker buildx build \
--push \ # sem --platform: runner já é ARM64
-t $IMAGE_TAG .O ponto crítico: no job de prod, o --platform linux/arm64 foi removido. Quando o runner já é ARM64, especificar a plataforma é redundante — e mantê-la pode confundir o buildx em algumas versões. O build passa a ser nativo sem nenhuma outra mudança.
O que foi removido do job de prod
docker/setup-qemu-action@v3 — desnecessário: runner já é ARM64
--platform linux/arm64 — desnecessário: arquitetura nativa do runner
Set deploy Key — não utilizado: deploy via kubectl, não SSH
Load Centralized Configuration — variáveis migradas para secrets/vars do repositório GitHub
Fix colateral: RBAC do kubectl rollout status
Após o build, o pipeline executa kubectl rollout status para aguardar o deploy completar antes de marcar o job como sucesso. O comando falhava silenciosamente com erro de permissão — a ServiceAccount do CI tinha apenas get, patch e update em deployments.
# Erro ao rodar kubectl rollout status
Error from server (Forbidden): deployments.apps "nextjs" is forbidden:
User "system:serviceaccount:myapp:cicd-deployer" cannot list resource
"deployments" in API group "apps" in the namespace "myapp"O rollout status precisa de list e watch para observar o progresso do rolling update. A Role foi atualizada:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: cicd-deployer-role
namespace: myapp
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch", "update"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list"]O segundo gargalo: imagem de 2,1 GB
Com o build resolvido, o próximo gargalo ficou evidente: o downtime durante rolling updates era de 4-5 minutos — não por lentidão do Kubernetes, mas pelo tempo de pull da imagem. Cada novo node no OKE precisava baixar 2,1 GB do OCIR (Oracle Container Registry), o que levava 1 minuto e 34 segundos por pod. Com 2 réplicas e rolling update sequencial, o window de indisponibilidade parcial era consistente.
A redução da imagem é o próximo passo: multi-stage build para excluir node_modules de dev, Next.js output: 'standalone' (reduz drasticamente o que vai para a imagem final) e otimização de layers. Uma imagem de ~500 MB reduziria o pull de 1m34s para ~20s — eliminando praticamente todo o downtime do rolling update.
Resultado
O primeiro deploy após a mudança para ubuntu-24.04-arm confirmou o funcionamento: build bem-sucedido, imagem publicada no OCIR, pods subindo no OKE com a nova tag.
# Validação pós-deploy
kubectl get pods -n myapp
# nextjs-7f4cbbdffd-g595h 1/1 Running 0 6m
# nextjs-7f4cbbdffd-gt5jt 1/1 Running 0 5m
kubectl get deployment nextjs -n myapp \
-o jsonpath='{.spec.template.spec.containers[0].image}'
# registry.example.com/myapp/www:abc1234fQEMU é uma ferramenta válida para builds pontuais em máquinas de desenvolvimento. Para CI/CD de produção onde cada commit passa pelo pipeline, a penalidade de 5-10x é inaceitável. Runners ARM nativos existem no GitHub Actions e custam o mesmo que runners x86 equivalentes — não há razão para manter QEMU em pipelines de longa duração.
Checklist para migrar seu pipeline
1. Identificar se o build usa --platform com arquitetura diferente do runner
2. Verificar se setup-qemu-action está presente (sinal claro de emulação)
3. Trocar runs-on para ubuntu-24.04-arm (ou runner ARM self-hosted)
4. Remover setup-qemu-action e --platform do job de prod
5. Verificar RBAC da ServiceAccount: rollout status precisa de list + watch
6. Medir tempo de pull da imagem — acima de 30s é candidato a otimização


