Guía práctica: cómo autoescalar runners self‑hosted en GitHub Actions para pipelines CI de proyectos PHP

Por · Publicado el · Actualizado el · 9 min de lectura

Profesional de TI usando portátil frente a racks de servidores con reflejo en el vidrio
Entorno de servidores y operación técnica, adecuado para temas de CI y autoscaling. Foto: Christina Morillo / Pexels

Guía práctica: cómo autoescalar runners self‑hosted en GitHub Actions para pipelines CI de proyectos PHP. Aquí aprenderás cuándo tiene sentido adoptar esta estrategia, cómo diseñar labels, usar runners efímeros, elegir patrones de autoscaling, configurar workflows y evitar trampas comunes. El objetivo es reducir costos y latencia sin comprometer seguridad ni fiabilidad.

Cuándo tiene sentido autoescalar runners self‑hosted

No todo pipeline necesita runners self‑hosted. Son especialmente útiles cuando buscas menor latencia, control de costes y entornos a medida. Considéralos cuando la cola en los runners hospedados crezca y los builds tarden más en arrancar.

Casos típicos en PHP: Composer con extensiones nativas, builds Docker de la aplicación, pruebas que acceden a recursos internos y pipelines que requieren red privada.

Si necesitas una visión general de CI/CD en PHP antes de avanzar, consulta nuestra guía de CI/CD para PHP/Laravel y luego vuelve al tema de autoscaling.

  • Control de costes por minuto y tamaño de máquina.
  • Menor latencia cerca del registry, base de datos y cache.
  • Entorno a medida: versiones de PHP, extensiones y caches precalentados.
  • Acceso seguro a redes privadas y artefactos internos.
  • Cumplimiento: aislamiento, auditoría y retención de logs.

Arquitectura: labels, runners efímeros y aislamiento

La base es simple: etiquetas tus runners y tus jobs solicitan esas labels. Usa runners efímeros para que cada job obtenga una VM/contenedor limpio y, al terminar, el runner se auto‑desregistre.

Define una taxonomía de labels que represente capacidades. El scheduler de GitHub empareja jobs y runners por labels, mejorando la predictibilidad y el paralelismo.

Para PHP, combina labels de versión, SO, Docker y tamaño. Esto facilita rutas específicas por stack y optimiza caches. Cuando integres con deploy, alinea las labels con tu pipeline de despliegue automático.

  • Ejemplos: self‑hosted, linux, php‑8‑2, php‑8‑3, docker, large, ssd.
  • Mantén labels estables y versionadas (php‑8‑3 en vez de latest).
  • Prefiere runners efímeros a persistentes para reducir deriva.
  • Separa entornos por sensibilidad: prod‑ci y sandbox‑ci.

Patrones de autoscaling: cuál usar en cada escenario

Existen tres patrones recurrentes. Elige según el nivel de control, la complejidad y el coste. Compara a continuación y selecciona el que se ajuste a tu operación y al nivel de habilidades del equipo.

Patrón Cómo escala Pros Contras Cuándo usar
Kubernetes + controlador de runners Pods bajo demanda; cada pod ejecuta un runner efímero Aislamiento fuerte; integración nativa; alta densidad Requiere k8s; curva de aprendizaje mayor Equipos con k8s y alta variación de carga
VMs con cloud-init/terraform + systemd Crea/termina VMs según la cola; cada VM corre 1 runner Máximo aislamiento; control de tamaño y región Provisionamiento más lento que pods Workloads pesados, builds Docker y seguridad estricta
VM única con múltiples contenedores runner Escala contenedores en el host según métricas Operación simple; costes menores Menos aislamiento; competencia por I/O y red Equipos pequeños y pipelines moderados

Setup mínimo de runner efímero en VM (script base)

Este ejemplo registra un runner efímero en un repositorio y ejecuta un único job. Requiere un token con permisos adecuados y acceso root para instalar dependencias. Adáptalo a tu nube y a la política de imágenes doradas.

El runner efímero elimina el registro al concluir. En casos concretos puedes invocar la eliminación explícitamente después del fin.

#!/usr/bin/env bash
set -euo pipefail

# Variáveis de ambiente esperadas:
# GH_REPO="owner/repo"  # alvo do runner
# GH_PAT  # token com escopo para criar registration tokens
# RUNNER_LABELS="self-hosted,linux,php-8-3,docker"

WORKDIR=/opt/actions-runner
mkdir -p "$WORKDIR" && cd "$WORKDIR"

# 1) Obter registration token de curta duração
REG_TOKEN=$(curl -s -X POST \
  -H "Authorization: Bearer ${GH_PAT}" \
  -H "Accept: application/vnd.github+json" \
  https://api.github.com/repos/${GH_REPO}/actions/runners/registration-token \
  | jq -r .token)

# 2) Baixar e preparar o runner (ajuste a versão conforme necessidade)
RUNNER_VERSION="2.317.0"
curl -L -o actions-runner.tar.gz \
  https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz

tar xzf actions-runner.tar.gz

# 3) Configurar como efêmero e etiquetar
./config.sh \
  --unattended \
  --url https://github.com/${GH_REPO} \
  --token "${REG_TOKEN}" \
  --name "runner-${HOSTNAME}-$(date +%s)" \
  --ephemeral \
  --labels "${RUNNER_LABELS}"

# 4) Executar um único job; o runner encerra após concluir
./run.sh

# Opcional: remoção explícita (caso tenha política de limpeza extra)
# REMOVE_TOKEN=$(curl -s -X POST \
#   -H "Authorization: Bearer ${GH_PAT}" \
#   -H "Accept: application/vnd.github+json" \
#   https://api.github.com/repos/${GH_REPO}/actions/runners/remove-token \
#   | jq -r .token)
# ./config.sh remove --unattended --token "${REMOVE_TOKEN}"

Workflow YAML: labels, cache de Composer y concurrencia

Apunta tus jobs a labels específicas. Usa cache de Composer y controla la concurrencia para evitar colas innecesarias. El ejemplo ejecuta tests en distintas versiones de PHP y limita ejecuciones simultáneas por rama.

Ajusta las labels a tu convención. Si usas Docker en el runner, asegúrate de que el usuario tenga permisos sin exponer el socket a jobs no confiables.

name: ci-php
on:
  push:
    branches: [ main ]
  pull_request:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    name: PHPUnit (PHP ${{ matrix.php }})
    runs-on: [self-hosted, linux, docker, php-${{ matrix.php }}]
    strategy:
      fail-fast: false
      matrix:
        php: [ '8-1', '8-2', '8-3' ]
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mbstring, intl, pdo_mysql, redis
          coverage: none
      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: |
            ~/.composer/cache
          key: composer-${{ runner.os }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            composer-${{ runner.os }}-${{ matrix.php }}-
      - name: Install deps
        run: composer install --no-interaction --prefer-dist --no-progress
      - name: Run tests
        run: vendor/bin/phpunit --colors=always
    timeout-minutes: 30

Orquestando el autoscaling: señales, límites y disparadores

Escalar bien es reaccionar a la cola y evitar el sobre‑aprovisionamiento. Mide el tiempo en cola de los jobs y el tiempo de bootstrap del runner, y ajusta el objetivo de instancias.

Crea disparadores de scale‑out por colas por encima de un umbral y scale‑in cuando la cola esté en cero durante un periodo. Respeta límites de cuota y aplica backoff exponencial al llamar APIs de registro para no superar rate limits.

Los tokens de registro expiran rápido. Généralos bajo demanda en el bootstrap y evita almacenarlos en disco. Precalienta imágenes para reducir el cold start.

  • Prioriza PRs recientes para reducir el ciclo de feedback.
  • Usa ventanas de enfriamiento entre eventos de escala.
  • Precalienta imágenes con PHP, Composer y extensiones.
  • Define límites máximos para impedir explosión de costes.
  • Cuotas por repositorio y organización, con auditoría.

Observabilidad, coste y rendimiento en pipelines PHP

Recolecta métricas de cola, tiempo de provisionamiento y duración del job. Relaciónalas con el tamaño de máquina y tipo de almacenamiento. Ajusta el autoscaling con datos, no solo con intuición.

Reduce costes acelerando los builds. Imágenes más pequeñas y cache eficiente impactan directamente. Si construyes imágenes Docker en el CI, el cache remoto de BuildKit ayuda mucho. Combínalo con buenas prácticas de multistage y, cuando proceda, con imágenes multiarquitectura.

  • Métricas clave: queue_time, runner_boot, job_duration, success_rate.
  • Monitorea consumo de CPU, memoria, disco y egress.
  • Bake de imagen: PHP, Composer, extensiones y herramientas de test.
  • Reutiliza capas Docker entre ramas con cache remoto.
  • Evita instalar toolchains en tiempo de job siempre que sea posible.

Seguridad de los runners self‑hosted: prácticas esenciales

Los runners ejecutan código no confiable en PRs. Aislar es obligatorio. Usa runners efímeros, usuarios sin privilegios y segmentación de red. Evita montar secretos innecesarios.

Restringe cuándo PRs desde forks pueden usar tus runners. Prefiere ejecutar solo validaciones seguras o usa entornos dedicados con privilegios mínimos y políticas de secretos estrictas.

Mantén las imágenes actualizadas. Rota tokens y limpia caches entre ejecuciones. Registra eventos de escala y accesos a runners para auditoría.

  • Principio de menor privilegio: no root en el job y sin docker.sock en PRs.
  • Secretos por entorno y repositorio, con alcance mínimo.
  • Bloquea puertos innecesarios y limita egress a destinos requeridos.
  • Actualiza el binario del runner y el SO periódicamente.
  • Limpieza agresiva de caches entre ejecuciones sensibles.

Preguntas frecuentes

¿Cuándo vale la pena migrar a runners self‑hosted?

Cuando la cola de jobs crece, el boot de los hosted añade latencia, necesitas extensiones nativas específicas, acceso a red privada o quieres controlar costes y tamaños de máquina. Empieza con un piloto en los pipelines más pesados.

¿Los runners efímeros eliminan el registro automáticamente?

Sí, el modo efímero detiene el runner tras un job y elimina el registro. Aun así, implementa rutinas defensivas de limpieza para fallos en el bootstrap o timeouts que puedan dejar registros huérfanos temporalmente.

¿Cómo evitar que PRs desde forks usen mis runners?

Separa workflows por evento. Evita disparar self‑hosted en pull_request desde forks. Reserva self‑hosted para ramas internas o usa entornos aislados con mínimos privilegios y políticas de secretos restringidas.

¿Cómo acelerar Composer en CI sin comprometer la seguridad?

Usa cache por versión de PHP y sistema, invalidado por composer.lock. Almacena vendor solo cuando sea necesario. Ejecuta install con flags de producción cuando aplique y limpia credenciales tras el job. Evita reusar caches entre proyectos distintos.

¿Qué métricas debo vigilar para el autoscaling?

Monitorea queue_time, tiempo de bootstrap del runner, duración del job, tasa de éxito y costes por job. También supervisa CPU, memoria, disco y red para detectar cuellos de botella. Ajusta límites de escala según series históricas.

¿Quieres mejorar el autoscaling de tus runners en GitHub Actions?

Si necesitas revisar labels, aislar la seguridad y reducir latencia en pipelines PHP, describe tu contexto y objetivos para trazar los próximos pasos.

Hablar sobre mi pipeline CI

Ver también