Tutorial: implementar deploy canario con GitHub Actions y Nginx para aplicaciones PHP monolito

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

Persona trabajando en escritorio con monitor grande y portátil, oficina luminosa al fondo
Entorno profesional con monitor y portátil, adecuado para tutoriales técnicos y operaciones de despliegue. Foto: RDNE Stock project / Pexels

Este tutorial práctico muestra cómo implementar un deploy canario con GitHub Actions y Nginx para aplicaciones monolito PHP (con smoke tests y rollback automático). Dividirás tráfico entre versiones, forzarás el canario por header/cookie, promoverás gradualmente y revertirás con seguridad.

Precondiciones y visión de la arquitectura canario

La estrategia canario reduce riesgos: un pequeño porcentaje del tráfico va a la nueva versión. Si todo se mantiene estable, la promueves; si no, la reviertes.

Usaremos Nginx para el enrutamiento ponderado y GitHub Actions para build, deploy, smoke tests, promoción y rollback. Dos pools PHP‑FPM aíslan las ejecuciones.

Componentes: Nginx al frente, dos pools PHP‑FPM (stable y canary), releases versionados y un workflow de CI que dispara acciones por SSH en el servidor.

Necesitas acceso root o sudo, claves SSH y un endpoint de salud, por ejemplo /health. Ten logs y métricas mínimas para detectar errores.

Para evitar problemas con estáticos, usa fingerprinting en archivos (ej.: app.abc123.css). Así evitas servir CSS/JS de la versión equivocada.

Prepara un plan de rollback simple y probado. Un checklist ayuda a no olvidar pasos críticos. Consulta el checklist de pre-deploy y rollback.

  • Nginx como reverse proxy y enrutador canario
  • Dos pools PHP‑FPM: stable y canary
  • Releases en /var/www/app/releases/<sha> + symlinks stable/canary
  • Workflow en GitHub Actions con SSH y scripts idempotentes
  • Endpoint /health para smoke tests y monitorización básica
  • Assets con fingerprint para evitar inconsistencias
  • Plan de rollback claro y probado

Configurar dos pools PHP‑FPM (stable y canary)

Crea dos pools PHP‑FPM para aislar opcache y parámetros entre stable y canary. Usan sockets y directorios distintos. Un reload de PHP‑FPM limpia el opcache y aplica cambios con mínima interrupción.

Ajusta pm.max_children según la capacidad. El canario recibe menos tráfico. Asegura permisos correctos en el directorio de la aplicación y en los sockets.

; /etc/php/8.2/fpm/pool.d/app.conf
[app_stable]
user = www-data
group = www-data
listen = /run/php-fpm-stable.sock
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
chdir = /var/www/app/releases/stable
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.validate_timestamps] = 1

[app_canary]
user = www-data
group = www-data
listen = /run/php-fpm-canary.sock
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chdir = /var/www/app/releases/canary
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.validate_timestamps] = 1

Configura Nginx para separar tráfico entre stable y canary por porcentaje usando split_clients. Permite forzar canario con el header X‑Canary: 1 o el cookie canary=1. La selección también define el docroot que usa PHP.

Respecto a assets: sirve estáticos desde stable por simplicidad. Usa fingerprinting para evitar inconsistencias visuales. Alternativas: CDN o reglas con rutas versionadas.

Si prefieres otra estrategia de intercambio de tráfico, considera un flujo blue‑green en contenedores. Hay una guía práctica al final de esta página.

Señal Cómo definirla Cuándo usarla
Porcentaje split_clients 5% canary Despliegue gradual por defecto sin intervención del usuario
Header X-Canary: 1 Forzar canario en smoke tests y QA
Cookie canary=1 Fijar usuario/grupo en el canario temporalmente
# Contexto http (ex.: nginx.conf)
map $http_x_canary $force_hdr_canary {
    default 0;
    "1" 1;
}
map $cookie_canary $force_ck_canary {
    default 0;
    "1" 1;
}
# Percentual no split_clients (editado via CI)
split_clients "$request_id" $bucket {
    5% canary;
    *  stable;
}
# Prioriza header/cookie; senão usa bucket
map "$force_hdr_canary$force_ck_canary$bucket" $pool {
    ~1            canary;
    ~.*canary     canary;
    default       stable;
}
map $pool $fpm_socket {
    stable  unix:/run/php-fpm-stable.sock;
    canary  unix:/run/php-fpm-canary.sock;
}
map $pool $docroot {
    stable  /var/www/app/releases/stable/public;
    canary  /var/www/app/releases/canary/public;
}

server {
    listen 80;
    server_name example.com;

    # Fallback de estáticos aponta para stable
    root /var/www/app/releases/stable/public;

    location /health {
        add_header Content-Type text/plain;
        return 200 "ok";
    }

    location / {
        try_files $uri /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $docroot$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT   $docroot;
        fastcgi_pass $fpm_socket;
    }

    # Estáticos: use fingerprinting (ex.: app.abc123.css)
    location ~* \.(?:css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
        expires 7d;
        try_files $uri @php_fallback;
    }
    location @php_fallback { rewrite ^ /index.php last; }
}

Pipeline CI: build y empaquetado en GitHub Actions

El workflow compila dependencias, empaqueta la aplicación y prepara variables para el deploy. Usa Composer con flags de producción y un artefacto único tar.gz.

La etapa de build está separada del deploy. Puedes ejecutar el workflow con workflow_dispatch y pasar el porcentaje inicial del canario.

Para organizar el pipeline siguiendo buenas prácticas, consulta la guía de pipeline de despliegue automático.

  • Composer en modo producción
  • Artefacto único versionado por SHA
  • Inputs del workflow para parametrizar el rollout
  • Separación clara de build y deploy
name: canary-deploy-php
on:
  workflow_dispatch:
    inputs:
      percent_start:
        description: 'Percentual inicial do canário'
        default: '5'
      host:
        description: 'Host de produção (SSH)'
        required: true
      app_url:
        description: 'URL pública para smoke tests'
        required: true
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      rel: ${{ steps.pkg.outputs.rel }}
    steps:
      - uses: actions/checkout@v4
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      - name: Install Composer deps
        run: |
          composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
      - name: Build assets (opcional)
        run: |
          echo 'adicione seu build de front aqui, se houver'
      - name: Package artifact
        id: pkg
        run: |
          REL=${GITHUB_SHA}
          tar -czf release-$REL.tar.gz --exclude-vcs --exclude=.github .
          echo rel=$REL >> $GITHUB_OUTPUT
      - uses: actions/upload-artifact@v4
        with:
          name: php-release-${{ steps.pkg.outputs.rel }}
          path: release-${{ steps.pkg.outputs.rel }}.tar.gz

Deploy del canario vía SSH y reloads sin downtime

En esta etapa enviamos el artefacto, lo extraemos en releases/<sha> y actualizamos el symlink canary. Ajustamos el porcentaje inicial en split_clients. Prueba la configuración con nginx -t y realiza reloads.

Guarda el release estable actual para poder hacer rollback rápido. El reload de PHP‑FPM limpia el opcache, evitando servir bytecode antiguo.

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: php-release-${{ needs.build.outputs.rel }}
      - name: Prep SSH
        env:
          SSH_KEY: ${{ secrets.SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
      - name: Ship and activate canary
        env:
          SSH_USER: ${{ secrets.SSH_USER }}
          SSH_HOST: ${{ inputs.host }}
          REL: ${{ needs.build.outputs.rel }}
          PSTART: ${{ inputs.percent_start }}
        run: |
          scp -o StrictHostKeyChecking=no release-$REL.tar.gz $SSH_USER@$SSH_HOST:/tmp/
          ssh -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST bash -s <<EOF
          set -euo pipefail
          REL=${REL}
          APP=/var/www/app
          sudo mkdir -p $APP/releases/$REL
          sudo tar -xzf /tmp/release-$REL.tar.gz -C $APP/releases/$REL
          # Registra release estável atual
          PREV=$(readlink -f $APP/releases/stable || true)
          echo "$PREV" | sudo tee $APP/prev_stable.txt >/dev/null
          # Ativa canary
          sudo ln -sfn $APP/releases/$REL $APP/releases/canary
          sudo systemctl reload php8.2-fpm
          # Ajusta percentual no split_clients (arquivo snippet)
          # Ex.: /etc/nginx/snippets/app_split.conf contém a linha "5% canary;"
          sudo sed -i "s/^[[:space:]]*[0-9]\+% canary;/ ${PSTART}% canary;/" /etc/nginx/snippets/app_split.conf || true
          sudo nginx -t && sudo systemctl reload nginx
          EOF

Smoke tests y promoción gradual automática

Con el canario activo, ejecuta smoke tests forzando el header X‑Canary: 1. Valida endpoints críticos. Si pasan, aumenta el porcentaje en pasos como 5→25→50→100.

Entre cada paso, espera y monitorea errores. Valida respuestas HTTP 200 y aborta ante el primer fallo. Integra alertas para actuar con rapidez. Consulta también cómo implementar health checks y readiness.

  • Forzar canario con header en todas las llamadas de prueba
  • Probar rutas de login, checkout y APIs críticas
  • Esperar y monitorizar 5xx entre promociones
  • Abortar y activar rollback ante el primer error
#!/usr/bin/env bash
# run on GitHub Actions or jump host
set -euo pipefail
APP_URL="$1"              # ex.: https://example.com
PERCENTS=(5 25 50 100)
SMOKE_ENDPOINTS=("/health" "/" "/api/status")

smoke() {
  local url="$1"; local fail=0
  for i in {1..10}; do
    for ep in "${SMOKE_ENDPOINTS[@]}"; do
      code=$(curl -fsS -o /dev/null -w "%{http_code}" -H "X-Canary: 1" "$url$ep" || true)
      if [[ "$code" != "200" ]]; then
        echo "Smoke FAIL $ep -> $code"; fail=1
      else
        echo "Smoke OK  $ep -> $code"
      fi
    done
    sleep 1
  done
  return $fail
}

for p in "${PERCENTS[@]}"; do
  echo "Promovendo canário para ${p}%..."
  ssh $SSH_OPTS $SSH_USER@$SSH_HOST "sudo sed -i 's/^[[:space:]]*[0-9]\+% canary;/ ${p}% canary;/' /etc/nginx/snippets/app_split.conf && sudo nginx -t && sudo systemctl reload nginx"
  echo "Aguardando estabilizar..."; sleep 60
  if ! smoke "$APP_URL"; then
    echo "Falha nos smoke tests em ${p}%. Iniciando rollback..."; exit 42
  fi
  echo "OK em ${p}%"
done

echo "100% canário aprovado. Promovendo a estável..."
ssh $SSH_OPTS $SSH_USER@$SSH_HOST 'APP=/var/www/app; sudo ln -sfn "$APP/releases/canary" "$APP/releases/stable" && sudo systemctl reload php8.2-fpm && sudo sed -i "s/^[[:space:]]*[0-9]\+% canary;/ 0% canary;/" /etc/nginx/snippets/app_split.conf && sudo nginx -t && sudo systemctl reload nginx'

Rollback automático y limpieza segura

Si algo falla, revierte de inmediato. Pone el canario a 0%, restaura el symlink stable al release anterior y recarga Nginx y PHP‑FPM. No borres el release fallido hasta que todo el tráfico esté dirigido a la estable.

Almacena la ruta del release estable antes del deploy y úsala en el rollback. Luego investiga con calma, fuera de horas pico.

#!/usr/bin/env bash
# rollback.sh (executar no runner)
set -euo pipefail
ssh $SSH_OPTS $SSH_USER@$SSH_HOST '
  set -euo pipefail
  APP=/var/www/app
  PREV=$(cat $APP/prev_stable.txt || true)
  echo "Restaurando stable -> $PREV"
  # 0% tráfego no canário
  sudo sed -i "s/^[[:space:]]*[0-9]\+% canary;/ 0% canary;/" /etc/nginx/snippets/app_split.conf || true
  sudo nginx -t && sudo systemctl reload nginx
  # Reaponta o estável
  if [ -n "$PREV" ] && [ -d "$PREV" ]; then
    sudo ln -sfn "$PREV" "$APP/releases/stable"
  fi
  sudo systemctl reload php8.2-fpm
'

Cómo confirmar que funcionó y siguientes pasos

Confirma que usuarios anónimos y autenticados acceden a la versión correcta según el porcentaje. Fuerza el canario con el header y valida el flujo completo: login, carrito, checkout y APIs.

Monitorea 5xx, tiempos de respuesta y logs de aplicación. Integra alertas al equipo. Al final, limpia releases antiguos y documenta porcentajes, tiempos y métricas.

¿Quieres una alternativa en contenedores con headers y pesos? Consulta el tutorial de canario con Traefik. Para intercambios totales, usa blue-green con Docker Compose para PHP. Has completado el tutorial para implementar deploy canario con GitHub Actions y Nginx en monolitos PHP.

  • Comprobar porcentajes con muestras controladas
  • Seguir logs y métricas durante 24–48 horas
  • Eliminar releases antiguos tras la validación
  • Documentar el rollout y los aprendizajes

Preguntas frecuentes

¿Puedo usar un único pool PHP‑FPM para stable y canary?

Puedes, pero pools separados aíslan opcache y parámetros, reduciendo riesgos de interferencia entre versiones. Con un pool único, invalida el opcache en cada release y comprueba que el límite de memoria soporta el cache de ambas versiones.

¿Cómo manejar assets (CSS/JS) diferentes entre versiones?

Usa fingerprinting en los nombres de archivos para que el navegador y Nginx sirvan el asset correcto aunque solo el tráfico dinámico esté en el canario. Alternativamente, publica estáticos en una CDN por versión y utiliza URLs absolutas versionadas.

¿Este flujo garantiza cero downtime?

Los reloads de Nginx y PHP‑FPM suelen ser gráciles y mantienen conexiones; la indisponibilidad tiende a ser mínima. Aun así, prueba la configuración con nginx -t y promueve en pasos cortos para reducir riesgos y evitar interrupciones perceptibles.

¿Y las migraciones de base de datos durante el canario?

Planifica migraciones compatibles con la versión antigua. Primero aplica cambios que no rompan la estable. Si la nueva versión requiere un schema distinto, usa feature flags y migra por etapas para permitir rollback sin pérdida.

¿Cómo monitorizar errores durante la promoción?

Recopila tasas de 5xx, latencias p95/p99 y errores de aplicación. Integra alertas en el canal del equipo. Si no tienes observabilidad, empieza con un endpoint /health y contadores simples; luego evoluciona hacia métricas y alertas más completas.

¿Puedo forzar un usuario específico al canario?

Sí. Define un cookie canary=1 al autenticar usuarios que quieras fijar en el canario. Nginx prioriza header y cookie sobre la muestra porcentual, permitiendo pruebas dirigidas y soporte QA sin depender del hash aleatorio.

¿Necesitas poner canarios en producción con seguridad?

Implementa rollout gradual, smoke tests y rollback automático en tu monolito PHP con apoyo especializado.

Hablar sobre deploy canario

Ver también