Blog
Nossas últimas novidadesGuia de CI/CD no Git para times pequenos: performance, cache e runners
Pipeline lento quase nunca é “só falta de CPU”.
Na prática, times pequenos sofrem com três dores recorrentes:
- Jobs esperando em fila porque caíram no runner “errado” (ou no runner “certo”, mas lotado).
- Build repetindo trabalho porque cache não existe, está mal configurado, ou invalida a cada commit.
- Builds Docker sempre “do zero” porque layer cache não está sendo reaproveitado.
Este guia é um passo a passo para você atacar essas dores sem depender de “milagre” e sem criar um CI/CD frágil.
- Se você usa GitLab CI/CD, GitHub Actions, ou ambos, este post cobre os conceitos e dá exemplos dos dois.
- Se você tem builds em ARM64 (Graviton / Apple Silicon) e x64, aqui tem um bloco específico para evitar armadilhas de arquitetura.
Mapa da série CI/CD
- Guia de CI/CD no Git para times pequenos: performance, cache e runners
- GitLab CI: tags de runner e roteamento de jobs
- Cache no CI/CD: como desenhar keys e evitar cache inválido (GitHub e GitLab)
- Docker layer caching no CI: BuildKit, buildx, cache-from e cache-to
- Runner dedicado no CI/CD: quando vale a pena e como planejar em time pequeno
- Runners macOS no CI: Intel vs Apple Silicon, custos e armadilhas
1) Diagnóstico rápido: o gargalo é fila ou execução?
Antes de otimizar, você precisa separar duas métricas.
- Tempo de fila (queue time): tempo entre o job ser criado e efetivamente começar.
- Tempo de execução (run time): tempo real que o job ficou rodando.
Isso importa porque as correções são diferentes:
- Fila alta → problema de capacidade/roteamento (runners).
- Execução alta → problema de build (cache, Docker layers, dependências, paralelismo).
1.1 Como medir sem ferramentas extras
-
Abra o log do job e anote:
- horário de criação
- horário de início
- horário de término
-
Calcule:
- fila = início - criação
- execução = término - início
- Faça isso para 5 a 10 execuções (não só uma). Pipeline “lento às vezes” quase sempre é concorrência + runner compartilhado + variação de rede/hardware.
1.2 Regra de bolso para times pequenos
- Se a fila for > 30% do tempo total, pare de “otimizar build” e comece por runners.
- Se a execução for > 70% do tempo total, seu ROI maior tende a ser cache e Docker layer caching.
2) Runners: como evitar job no runner “errado”
O sintoma clássico é: “às vezes é rápido, às vezes fica 10 minutos esperando”.
Em geral isso acontece quando:
- Falta critério para direcionar jobs (tags/labels).
- Existe um runner “genérico” aceitando tudo (inclusive jobs pesados).
- O pipeline depende de máquinas de desenvolvimento (Wi‑Fi, uso local, variação de hardware).
2.1 GitLab: tags de runner (e o que quase todo mundo erra)
No GitLab, tags de CI/CD servem para controlar quais jobs um runner pode executar (não confundir com Git tags de commits). Um job só roda em um runner que tenha todas as tags definidas no job. Se o runner estiver configurado para rodar apenas jobs com tag, jobs sem tag podem ficar “stuck”.
Leitura complementar (satélite):
Exemplo de job com tag dedicada:
build_web:
stage: build
tags:
- linux-x64
- docker
script:
- npm ci
- npm run buildBoas práticas para time pequeno:
- Use poucas tags, com significado claro (ex.:
linux-x64,linux-arm64,macos,docker). - Evite tags “genéricas” que viram uma lixeira (ex.:
build,runner1). - Defina um runner “padrão” apenas para jobs leves (lint/test rápido), e runners separados para builds pesados.
2.2 GitHub Actions: runs-on, labels e runner groups
No GitHub Actions, você direciona execução com runs-on. Para runners self-hosted, você pode usar labels e runner groups para segmentar quem roda o quê.
Exemplo simples (hosted runner):
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm testExemplo com runner self-hosted (labels):
jobs:
build:
runs-on: [self-hosted, linux, x64, build-heavy]
steps:
- uses: actions/checkout@v4
- run: ./ci/build.shLeitura complementar (satélite):
3) Cache no CI/CD: dependências rápidas e builds previsíveis
Cache não é “acelerador mágico”. É um contrato.
- Se o cache “acerta”, você economiza minutos.
- Se o cache “erra”, você cria bugs difíceis (artefato antigo, dependência incompatível, build contaminado).
Leitura complementar (satélite):
3.1 Cache vs artifacts: não misture
- Cache: reuso de dependências e diretórios que podem ser reconstruídos (ex.: cache do npm/pnpm, Gradle, pip).
- Artifacts: saída do build que será consumida por outro job (ex.: pasta
dist/, binários, relatórios).
Regra prática: se outro job precisa do arquivo “como resultado”, use artifact. Se é só para acelerar repetição, use cache.
3.2 GitHub Actions: key e restore-keys (o básico que resolve 80%)
Um bom cache key normalmente inclui:
- SO e arquitetura
- Hash do lockfile
- Versão de runtime (quando necessário)
Exemplo para Node (cachê de dependências):
- name: Cache npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-${{ runner.arch }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-npm-Erros comuns:
- Cachear diretórios “grandes e instáveis” (ex.:
node_modules) sem critério. - Esquecer arquitetura no key (quebra quando você tem ARM e x64).
- Incluir “o commit” no key (invalida toda vez e não serve para nada).
3.3 GitLab CI/CD: cache key e fallback_keys
No GitLab, você pode usar cache:key e chaves de fallback para aproveitar cache mesmo em branches novas (quando faz sentido).
Exemplo:
cache:
key: "node-${CI_RUNNER_EXECUTABLE_ARCH}-${CI_COMMIT_REF_SLUG}"
paths:
- .npm/
fallback_keys:
- "node-${CI_RUNNER_EXECUTABLE_ARCH}-main"
- "node-${CI_RUNNER_EXECUTABLE_ARCH}-default"Dicas:
- Separe cache por arquitetura (
CI_RUNNER_EXECUTABLE_ARCHou variável equivalente). - Use fallback para “pegar o cache da main” quando a branch é nova (bom para dependências).
- Evite fallback para outputs de build (isso é artifact, não cache).
4) Docker layer caching: quando o build “come” o pipeline
Se seu pipeline faz docker build e demora muito, existem dois cenários:
- Você está rebuildando camadas que poderiam ser reaproveitadas.
- Seu Dockerfile invalida cache cedo (ex.: copia o repo inteiro antes de instalar dependências).
Leitura complementar (satélite):
4.1 Primeiro: conserte o Dockerfile
Checklist de ouro (multi-stage e cache friendly):
- Copie primeiro apenas manifestos (ex.:
package.json,package-lock.json). - Instale dependências.
- Só depois copie o resto do código.
- Separe build e runtime (multi-stage).
Exemplo (Node):
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY /app/dist /usr/share/nginx/html4.2 Depois: use cache de layers no CI
Duas estratégias comuns:
- Registry cache (recomendado): o cache vai e volta via registry.
- Cache local (bom para runner dedicado): o runner mantém camadas localmente.
Um padrão simples é usar BuildKit/buildx com --cache-from e --cache-to apontando para o registry.
5) ARM64 vs x64: por que isso quebra cache e (às vezes) o build
Arquitetura não é detalhe.
Se você tem runners ARM64 e x64 rodando o mesmo pipeline, isso afeta:
- Caches (dependências nativas não são compatíveis entre arquiteturas).
- Docker images (precisam existir para a arquitetura ou usar emulação, que é mais lenta).
- Binários pré-compilados (ex.: pacotes com addons nativos).
5.1 Regras práticas para não sofrer
- Inclua arquitetura no cache key.
- Tenha tags/labels explícitas por arquitetura.
- Se buildar Docker multi-arch, trate como pipeline separado (ou matrix com estratégia).
Exemplo (GitHub Actions com matrix de arch):
strategy:
matrix:
arch: [x64, arm64]
jobs:
build:
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
steps:
- uses: actions/checkout@v4
- run: ./ci/build.shExemplo (GitLab com tags por arch):
build_amd64:
tags: [linux-x64, docker]
script: ["./ci/build.sh"]
build_arm64:
tags: [linux-arm64, docker]
script: ["./ci/build.sh"]6) macOS runners: quando você precisa e como evitar armadilhas
macOS runner normalmente entra por necessidade:
- build iOS/macOS (Xcode)
- codesign/notarization
- testes específicos de macOS
Leitura complementar (satélite):
6.1 Dicas rápidas
- Evite rodar jobs “genéricos” no macOS: reserve para o que realmente precisa.
- Prefira runner dedicado para macOS (custo alto por hora e fila dói).
- Cacheie com cuidado (Xcode DerivedData pode ajudar, mas também pode contaminar se for compartilhado sem estratégia).
Checklist final: plano de 1 dia para acelerar seu CI/CD
- Medir fila vs execução em 5 a 10 execuções.
- Definir “classes” de jobs (leve, pesado, macOS, Docker build).
- Criar tags/labels claras e remover runner “lixeira” que aceita tudo.
- Separar cache por arquitetura (ARM64/x64) e por lockfile hash.
- Ajustar Dockerfile para não invalidar cache cedo.
- Ativar Docker layer caching (registry cache ou cache local em runner dedicado).
- Documentar: “qual job roda em qual runner e por quê” (evita regressão).
Referências
- GitLab Docs: Configuring runners (tags, jobs tagged/untagged)
- GitLab Docs: Caching in GitLab CI/CD
- GitLab Docs: GitLab.com hosted runners on Linux (tags por tamanho/arquitetura)
- GitLab Docs: Make Docker-in-Docker builds faster with Docker layer caching
- GitHub Docs: Caching dependencies and build outputs
- GitHub Docs: Choose the runner for a job
- GitHub Docs: GitHub-hosted runner reference
- Imagem de capa: Xavier Cee (Unsplash)