Tutorial: configurar runners efêmeros Docker no GitHub Actions com cache remoto para acelerar builds de monólitos PHP — passo a passo

Deploy & DevOps · Por · Publicado em · 11 min de leitura

Escada rolante dupla em espaço interno com painéis de luzes lineares acima
Composição moderna que sugere movimento e fluxo de processos de infraestrutura. Foto: Thiestro / Pixabay

Tutorial: configurar runners efêmeros Docker no GitHub Actions com cache remoto para acelerar builds de monólitos PHP. Aqui você vai ver a arquitetura, a imagem do runner, o Docker Compose, o workflow YAML com BuildKit/Buildx e como ajustar o Dockerfile para aproveitar cache entre execuções, reduzindo tempo de build sem abrir mão de isolamento.

Pré-requisitos e quando faz sentido

Runners efêmeros são ideais quando você quer isolamento por job, alto paralelismo e zero acúmulo de estado entre execuções. O custo é perder caches locais; por isso, usaremos cache remoto.

Este tutorial — configurar runners efêmeros Docker no GitHub Actions com cache remoto para acelerar builds de monólitos PHP — foca em monólitos PHP (ex.: apps Laravel) que constroem imagens Docker.

Você precisa de um host Linux com Docker e acesso para criar containers do runner. O repositório no GitHub deve permitir runners self-hosted.

  • Host Linux com Docker e Docker Compose (ou Compose Plugin).
  • Acesso ao repositório ou organização no GitHub para criar token de runner.
  • Git e SSH opcionais para pull de submódulos.
  • Variáveis seguras para PAT (se for buscar token via API).

Arquitetura: runner efêmero + Docker + cache remoto

Cada job sobe em um container com o GitHub Actions Runner, registra-se com token efêmero, executa um único job e encerra. O build Docker é feito via BuildKit/Buildx usando cache remoto persistente entre execuções.

Como o runner é destruído após o job, o cache local do Docker não ajuda. Por isso, adotamos backends remotos de cache do BuildKit, adequados a ambientes efêmeros. Veja opções comuns:

Backend de cache Persiste entre runners? Complexidade Quando usar
local (padrão) Não Baixa Ambientes fixos ou runners persistentes.
registry (type=registry) Sim Média Você controla um registry; bom para times maiores.
GHA (type=gha) Sim Baixa Integra-se ao GitHub Actions; ótimo para começar.

Criar a imagem do runner efêmero em Docker

Usaremos a imagem oficial do runner como base e um entrypoint que registra em modo efêmero, executa o job e remove o runner. Montaremos o socket do Docker no container para permitir builds dentro do job. Montar o socket concede privilégios elevados ao container; use host dedicado e políticas de acesso.

# Crie Dockerfile e entrypoint do runner
mkdir -p runner-image && cd runner-image

cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM ghcr.io/actions/runner:latest

# Instale Docker CLI para usar o socket do host a partir do runner
USER root
RUN apt-get update \ 
 && apt-get install -y --no-install-recommends ca-certificates curl gnupg \ 
 && install -m 0755 -d /etc/apt/keyrings \ 
 && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ 
 && chmod a+r /etc/apt/keyrings/docker.gpg \ 
 && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list \ 
 && apt-get update \ 
 && apt-get install -y --no-install-recommends docker-ce-cli \ 
 && apt-get purge -y --auto-remove curl gnupg \ 
 && rm -rf /var/lib/apt/lists/*

# Entrypoint para registro efêmero
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

USER 1001
ENTRYPOINT ["/entrypoint.sh"]
EOF

cat > entrypoint.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

: "${RUNNER_URL:?Defina RUNNER_URL (repo ou org)}"
: "${RUNNER_TOKEN:?Defina RUNNER_TOKEN válido}"
: "${RUNNER_LABELS:=docker,linux,x64,ephemeral}"
: "${RUNNER_NAME:=runner-$(hostname)-$RANDOM}"
: "${RUNNER_WORKDIR:=/_work}"

cleanup() {
  echo "Removendo runner..."
  ./config.sh remove --unattended --token "$RUNNER_TOKEN" || true
}
trap cleanup EXIT

./config.sh \
  --url "$RUNNER_URL" \
  --token "$RUNNER_TOKEN" \
  --work "$RUNNER_WORKDIR" \
  --labels "$RUNNER_LABELS" \
  --name "$RUNNER_NAME" \
  --ephemeral \
  --unattended \
  --replace

exec ./run.sh
EOF

# Construa a imagem do runner
docker build -t gha-ephemeral-runner:latest .

Orquestrar runners com Docker Compose e token dinâmico

O token de registro expira rapidamente, então gere um token novo antes de subir cada runner. Abaixo, um script simples usando PAT com escopo para ler o repositório e criar token de registro. Proteja o PAT em variável de ambiente segura. Você pode escalar múltiplos runners com Compose.

Para controle de recursos do host, veja como limitar CPU/memória em containers no guia de cgroup v2: cgroup v2 + systemd para Docker.

# Arquivos: get-token.sh, .env e docker-compose.yml
mkdir -p ../runner-compose && cd ../runner-compose

# Script para obter token de registro (repo)
cat > get-token.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
# Variáveis exigidas: GH_OWNER, GH_REPO, GH_PAT
: "${GH_OWNER:?}"
: "${GH_REPO:?}"
: "${GH_PAT:?}"

resp=$(curl -s -X POST \
  -H "Authorization: token ${GH_PAT}" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/actions/runners/registration-token)

echo "$resp" | jq -r .token
EOF
chmod +x get-token.sh

# Variáveis do ambiente do Compose
cat > .env <<'EOF'
GH_OWNER=seu-usuario-ou-org
GH_REPO=seu-repo
# Export GH_PAT no shell ao rodar o compose para não gravar no disco
RUNNER_URL=https://github.com/${GH_OWNER}/${GH_REPO}
RUNNER_LABELS=docker,linux,x64,ephemeral
EOF

# Docker Compose do runner efêmero
cat > docker-compose.yml <<'EOF'
services:
  gha-runner:
    image: gha-ephemeral-runner:latest
    restart: "no"
    environment:
      RUNNER_URL: ${RUNNER_URL}
      RUNNER_LABELS: ${RUNNER_LABELS}
      RUNNER_TOKEN: ${RUNNER_TOKEN}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 4g
EOF

# Exemplo: iniciar 2 runners efêmeros
export GH_PAT=coloque_seu_token_aqui
export RUNNER_TOKEN=$(./get-token.sh)
RUNNER_TOKEN="$RUNNER_TOKEN" docker compose up -d --scale gha-runner=2

Workflow do GitHub Actions com BuildKit e cache remoto

Agora configure o workflow para usar o runner self-hosted por rótulos e habilitar cache remoto do BuildKit (type=gha) e cache do Composer. Se sua pipeline também constrói imagens multi-arquitetura, veja o tutorial de manifestos: Como criar imagens Docker multi‑arquitetura. Para fundamentos de cache remoto, confira: BuildKit e cache remoto no CI.

Com essa configuração, você aplica na prática o tutorial: configurar runners efêmeros Docker no GitHub Actions com cache remoto para acelerar builds de monólitos PHP, reduzindo tempo de build de forma previsível.

name: CI PHP (runner efêmero)

on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  build:
    runs-on: [self-hosted, linux, x64, ephemeral]
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configurar Buildx
        uses: docker/setup-buildx-action@v3

      - name: Cache do Composer
        uses: actions/cache@v4
        with:
          path: |
            ~/.cache/composer
          key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            composer-${{ runner.os }}-

      - name: Instalar dependências PHP (Composer)
        run: |
          echo "COMPOSER_CACHE_DIR=$HOME/.cache/composer" >> $GITHUB_ENV
          composer install --no-interaction --prefer-dist

      - name: Build da imagem com cache remoto (BuildKit)
        env:
          DOCKER_BUILDKIT: 1
        run: |
          docker buildx build \
            --cache-from type=gha \
            --cache-to type=gha,mode=max \
            -t ghcr.io/${{ github.repository }}/app:${{ github.sha }} \
            .

      - name: Testes (exemplo)
        run: |
          vendor/bin/phpunit --colors=always --testdox

Ajuste do Dockerfile para monólitos PHP com camadas cacheáveis

Para acelerar ainda mais, estruture seu Dockerfile com multistage e mounts de cache do BuildKit. Separe dependências PHP (Composer) das camadas de código, minimize invalidações e use COPY seletivo. O exemplo abaixo usa Composer 2 como estágio de deps e PHP-FPM como runtime. Adapte extensões e ini conforme seu projeto.

# syntax=docker/dockerfile:1.7

########## deps ##########
FROM composer:2 AS deps
WORKDIR /app

# Apenas arquivos que influenciam as deps
COPY composer.json composer.lock ./
# Cache do Composer isolado
RUN --mount=type=cache,target=/tmp/composer-cache \
    COMPOSER_CACHE_DIR=/tmp/composer-cache \
    composer install --no-dev --no-interaction --prefer-dist --no-progress

########## builder ##########
FROM php:8.2-fpm-alpine AS builder
WORKDIR /app

# Extensões necessárias (exemplo)
RUN apk add --no-cache icu-dev libzip-dev oniguruma-dev git \
 && docker-php-ext-install intl zip mbstring

# Copie apenas o necessário para build
COPY --from=deps /app/vendor /app/vendor
COPY . .

# Opcional: build de assets se houver (npm/yarn) usando cache
# RUN --mount=type=cache,target=/root/.npm \
#     npm ci && npm run build

########## runtime ##########
FROM php:8.2-fpm-alpine AS runtime
WORKDIR /var/www/html

# Extensões de runtime
RUN apk add --no-cache icu libzip oniguruma \
 && docker-php-ext-install opcache

# Ajustes de opcache (exemplo)
RUN { \
  echo 'opcache.enable=1'; \
  echo 'opcache.validate_timestamps=0'; \
  echo 'opcache.jit=1255'; \
} > /usr/local/etc/php/conf.d/opcache.ini

COPY --from=builder /app /var/www/html

HEALTHCHECK --interval=30s --timeout=3s CMD php -v || exit 1

CMD ["php-fpm"]

Verificação e solução de problemas

No log do job, o nome do runner indica execução em self-hosted. Após o step de Buildx, procure por mensagens de cache hit/miss. Quando o cache remoto funciona, as camadas estáveis mostram recuperação rápida.

No step do Composer, o actions/cache indicará se restaurou a chave primária ou parcial. Em reexecuções com composer.lock inalterado, a instalação será visivelmente mais rápida.

Token inválido ou expirado: gere um novo imediatamente antes do docker compose up. Confirme escopos do PAT.

Build sem cache: garanta DOCKER_BUILDKIT=1, use docker/setup-buildx-action e mantenha as flags --cache-from/--cache-to consistentes.

Erro ao acessar /var/run/docker.sock: confira permissões do socket no host e se o volume está montado.

Instabilidade de rede: caches remotos dependem de boa conectividade; considere retentativas automáticas no job.

  • Verifique labels do runner no cabeçalho do job.
  • Procure por “Loaded cache”/“Reusing cache” no build.
  • Confira o resumo do actions/cache para Composer.
  • Valide variáveis RUNNER_URL, RUNNER_TOKEN e labels idênticas às do runs-on.
  • Mantenha o Dockerfile estável para maximizar reaproveitamento.
  • Evite copiar o diretório inteiro antes das deps no Dockerfile.

Segurança e próximos passos

Montar o socket do Docker dá ao container controle total do host. Isole o host, limite quem dispara workflows e audite secrets. Avalie rodar em hosts descartáveis ou com políticas de reimage. Veja o checklist: Checklist de segurança CI/CD.

Para crescer, implemente autoscaling que cria containers do runner sob demanda a partir de webhooks/filas. Guia: auto‑escalar runners self‑hosted no GitHub Actions. Se sua pipeline também publica imagens multi‑arch, confira: Como criar imagens Docker multi‑arquitetura.

Para fundamentos de cache e boas práticas de BuildKit em CI, veja: BuildKit e cache remoto no CI.

  • Use redes e firewalls específicos ao host dos runners.
  • Restrinja PATs e tokens, rotacionando com frequência.
  • Monitore consumo e limites com métricas do host.

Perguntas frequentes

Qual a diferença entre runner efêmero e fixo?

O runner efêmero registra, executa um único job e se remove. Não mantém estado local, reduzindo deriva e riscos. O runner fixo permanece registrado, pode acumular dependências e caches locais, o que agiliza, mas aumenta necessidade de manutenção e hardening.

O cache remoto do BuildKit funciona com qualquer Dockerfile?

Funciona, porém o ganho depende de como o Dockerfile foi escrito. Multistage, ordem de COPY e uso de mounts de cache ajudam muito. Se camadas mudam sempre, o cache terá pouco efeito. Ajuste o Dockerfile para maximizar reaproveitamento.

Preciso usar registry privado para cache remoto?

Não. Você pode usar o backend type=gha, que grava o cache no próprio GitHub Actions. Registries privados são úteis para equipes que já o utilizam e desejam reuso de cache entre projetos e fora do GitHub.

É seguro montar o socket do Docker no runner?

Traz riscos, pois concede controle de root no host. Use host dedicado, restrinja quem pode executar pipelines, limite escopos de tokens e monitore. Alternativas incluem Docker-in-Docker com isolamento extra, porém com custo de performance e complexidade.

Como obter o token de registro do runner?

Gere via API do GitHub com um Personal Access Token e chame o endpoint de criação de token de runner para o repositório (ou organização). O token expira rápido, então busque-o pouco antes de iniciar o container do runner.

Composer deve cachear vendor ou o cache interno?

Prefira o cache interno do Composer (~/.cache/composer) via actions/cache. Ele acelera downloads sem arriscar inconsistências. Cache de vendor pode ser útil, mas requer chaves por SO/PHP/extensões e pode invalidar com pequenas mudanças.

Quer adaptar esse setup ao seu pipeline PHP?

Podemos revisar seu Dockerfile, workflow e estratégia de cache para reduzir o tempo de build com segurança.

Falar sobre CI com runners efêmeros

Veja também