Tutorial: configurar runners efímeros Docker en GitHub Actions con cache remoto para acelerar builds de monolitos PHP — paso a paso

Deploy & DevOps · Por · Publicado el · 11 min de lectura

Escalera mecánica doble en un espacio interior con paneles de luces lineales arriba
Composición moderna que sugiere movimiento y flujo de procesos de infraestructura. Foto: Thiestro / Pixabay

Tutorial para configurar runners efímeros Docker en GitHub Actions con cache remoto para acelerar builds de monolitos PHP. Aquí verás la arquitectura, la imagen del runner, Docker Compose, el workflow YAML con BuildKit/Buildx y cómo ajustar el Dockerfile para aprovechar cache entre ejecuciones, reduciendo tiempo de build sin perder aislamiento.

Precondiciones y cuándo tiene sentido

Los runners efímeros son ideales cuando buscas aislamiento por job, alto paralelismo y cero acumulación de estado entre ejecuciones. El coste es perder caches locales; por eso adoptamos cache remoto.

Este tutorial — configurar runners efímeros Docker en GitHub Actions con cache remoto para acelerar builds de monolitos PHP — se centra en monolitos PHP (p. ej., apps Laravel) que construyen imágenes Docker.

Necesitas un host Linux con Docker y permisos para crear contenedores del runner. El repositorio en GitHub debe permitir runners self-hosted.

  • Host Linux con Docker y Docker Compose (o Compose Plugin).
  • Acceso al repositorio u organización en GitHub para crear token de registro del runner.
  • Git y SSH opcionales para pull de submódulos.
  • Variables seguras para el PAT (si obtienes token vía API).

Arquitectura: runner efímero + Docker + cache remoto

Cada job se ejecuta en un contenedor con el GitHub Actions Runner, se registra con token efímero, ejecuta un único job y se detiene. El build Docker se realiza con BuildKit/Buildx usando cache remoto persistente entre ejecuciones.

Como el runner se destruye tras el job, el cache local de Docker no ayuda. Por eso adoptamos backends remotos de cache de BuildKit, adecuados para entornos efímeros. Opciones comunes:

Backend de cache ¿Persiste entre runners? Complejidad Cuándo usar
local (predeterminado) No Baja Entornos fijos o runners persistentes.
registry (type=registry) Sí Media Controlas un registry; bueno para equipos grandes.
GHA (type=gha) Sí Baja Se integra con GitHub Actions; excelente para comenzar.

Crear la imagen del runner efímero en Docker

Usaremos la imagen oficial del runner como base y un entrypoint que registra en modo efímero, ejecuta el job y elimina el runner. Montaremos el socket de Docker en el contenedor para permitir builds dentro del job. Montar el socket concede privilegios elevados al contenedor; usa un host dedicado y políticas de acceso.

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

Orquestar runners con Docker Compose y token dinámico

El token de registro expira rápidamente, así que genera un token nuevo antes de levantar cada runner. Abajo hay un script sencillo que usa un PAT con scope para leer el repositorio y crear el token de registro. Protege el PAT en una variable de entorno segura. Puedes escalar múltiples runners con Compose.

Para control de recursos del host, mira cómo limitar CPU/memoria en contenedores en la guía de cgroup v2: cgroup v2 y 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 de GitHub Actions con BuildKit y cache remoto

Ahora configura el workflow para usar el runner self-hosted por etiquetas y habilitar cache remoto de BuildKit (type=gha) y cache de Composer. Si tu pipeline también construye imágenes multiarquitectura, consulta Cómo crear imágenes Docker multi‑arquitectura.

Para fundamentos de cache remoto, consulta BuildKit y cache remoto en CI.

Con esta configuración aplicarás en la práctica el tutorial: configurar runners efímeros Docker en GitHub Actions con cache remoto para acelerar builds de monólitos PHP, reduciendo tiempo de build de forma predecible.

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 del Dockerfile para monolitos PHP con capas cacheables

Para acelerar más, estructura tu Dockerfile con multistage y mounts de cache de BuildKit. Separa dependencias PHP (Composer) de las capas de código, minimiza invalidaciones y usa COPY selectivo. El ejemplo abajo usa Composer 2 como etapa de deps y PHP-FPM como runtime. Adapta extensiones e ini según tu proyecto.

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

Verificación y solución de problemas

En el log del job, el nombre del runner indica ejecución en self-hosted. Tras el paso de Buildx, busca mensajes de cache hit/miss. Cuando el cache remoto funciona, las capas estables muestran recuperación rápida.

En el step de Composer, actions/cache indicará si restauró la clave primaria o parcial. En reejecuciones con composer.lock sin cambios, la instalación será visiblemente más rápida.

Token inválido o expirado: genera uno nuevo inmediatamente antes de docker compose up. Confirma scopes del PAT.

Build sin cache: asegúrate de DOCKER_BUILDKIT=1, usa docker/setup-buildx-action y mantén las flags --cache-from/--cache-to consistentes.

Error al acceder a /var/run/docker.sock: revisa permisos del socket en el host y que el volumen esté montado.

Inestabilidad de red: los caches remotos dependen de buena conectividad; considera reintentos automáticos en el job.

  • Verifica las labels del runner en el encabezado del job.
  • Busca “Loaded cache”/“Reusing cache” en el build.
  • Consulta el resumen de actions/cache para Composer.
  • Valida variables RUNNER_URL, RUNNER_TOKEN y que las labels coincidan con runs-on.
  • Mantén el Dockerfile estable para maximizar reaprovechamiento.
  • Evita copiar todo el directorio antes de las deps en el Dockerfile.

Seguridad y siguientes pasos

Montar el socket de Docker da al contenedor control total del host. Aísla el host, limita quién dispara workflows y audita secrets. Evalúa ejecutar en hosts desechables o con políticas de reimage. Revisa el checklist de seguridad CI/CD.

Para escalar, implementa autoscaling que cree contenedores del runner bajo demanda a partir de webhooks/colas. Guía: autoescalar runners self‑hosted en GitHub Actions. Si tu pipeline publica imágenes multi‑arch, consulta Cómo crear imágenes Docker multi‑arquitectura.

Para fundamentos de cache y buenas prácticas de BuildKit en CI, revisa BuildKit y cache remoto en CI.

  • Usa redes y firewalls específicos para los hosts de runners.
  • Restringe PATs y tokens, rotándolos con frecuencia.
  • Monitorea consumo y límites con métricas del host.

Preguntas frecuentes

¿Cuál es la diferencia entre un runner efímero y uno fijo?

El runner efímero se registra, ejecuta un único job y se elimina. No mantiene estado local, reduciendo deriva y riesgos. El runner fijo permanece registrado, puede acumular dependencias y caches locales, lo que agiliza builds pero exige más mantenimiento y hardening.

¿El cache remoto de BuildKit funciona con cualquier Dockerfile?

Funciona, pero la ganancia depende de cómo esté escrito el Dockerfile. Multistage, orden de COPY y uso de mounts de cache ayudan mucho. Si las capas cambian siempre, el cache tendrá poco efecto. Ajusta el Dockerfile para maximizar el reaprovechamiento.

¿Necesito un registry privado para cache remoto?

No. Puedes usar el backend type=gha, que guarda el cache en GitHub Actions. Registries privados son útiles si ya los usas y quieres reuso de cache entre proyectos o fuera de GitHub.

¿Es seguro montar el socket de Docker en el runner?

Supone riesgos, pues otorga control de root sobre el host. Usa hosts dedicados, restringe quién puede ejecutar pipelines, limita scopes de tokens y monitoriza. Alternativas incluyen Docker-in-Docker con aislamiento adicional, aunque con coste en rendimiento y complejidad.

¿Cómo obtener el token de registro del runner?

Genera el token vía API de GitHub con un Personal Access Token y llama al endpoint de creación de token de runner para el repositorio u organización. El token expira rápido, así que solicítalo justo antes de iniciar el contenedor del runner.

¿Composer debe cachear vendor o el cache interno?

Prefiere el cache interno de Composer (~/.cache/composer) vía actions/cache. Acelera descargas sin arriesgar inconsistencias. Cachear vendor puede ayudar, pero requiere claves por SO/PHP/extensiones y puede invalidarse con pequeños cambios.

¿Quieres adaptar este setup a tu pipeline PHP?

Podemos revisar tu Dockerfile, workflow y estrategia de cache para reducir el tiempo de build con seguridad.

Hablar sobre CI con runners efímeros

Ver también