Files

14 KiB

Despliegue — CI/CD con GitHub Actions

Este documento describe cómo quedan configurados el pipeline de build/publish a GHCR y el despliegue automático en el servidor privado, y qué pasos manuales (una sola vez) hacen falta para que funcione.

Los workflows viven en .github/workflows/deploy-backend.yml y .github/workflows/deploy-frontend.yml. Cada uno tiene dos jobs: build (en un runner de GitHub, compila y publica la imagen en GHCR) y deploy (en el runner self-hosted del servidor, la baja y reinicia el contenedor correspondiente). Un push a develop que solo toca Club12-Backend/** dispara únicamente el workflow de backend, y viceversa; un cambio en docker-compose.yml o en los workflows dispara ambos.

1. Prerrequisitos del servidor

  • Host Debian con Docker Engine + Docker Compose v2 instalados (docker compose version debe responder v2.x).

  • El usuario que corre el runner (ghrunner) debe pertenecer al grupo docker:

    sudo usermod -aG docker ghrunner
    # cerrar sesión y volver a entrar (o `newgrp docker`) para que el cambio de grupo tome efecto
    

    Verificar como ese usuario, sin sudo:

    docker ps
    

    Si falla por permisos, el grupo no se aplicó todavía.

2. Registrar los runners self-hosted (manual, una vez por runner)

Se necesitan dos runners registrados por separado, uno por cada runs-on label. Un mismo runner no puede sostener dos jobs en simultáneo.

Obtener un token de registro en: https://github.com/FrancoRu/Taller-de-Integracion/settings/actions/runners/new

Backend, en /home/ghrunner/actions-runner (o el directorio equivalente):

./config.sh --url https://github.com/FrancoRu/Taller-de-Integracion \
            --token <REGISTRATION_TOKEN> \
            --name club12-back \
            --labels Club-12-back-runner \
            --work _work
sudo ./svc.sh install ghrunner && sudo ./svc.sh start

Repetir en un segundo directorio de runner, separado, con:

./config.sh --url https://github.com/FrancoRu/Taller-de-Integracion \
            --token <REGISTRATION_TOKEN> \
            --name club12-front \
            --labels Club-12-front-runner \
            --work _work
sudo ./svc.sh install ghrunner && sudo ./svc.sh start

Notas importantes:

  • self-hosted se agrega automáticamente al registrar el runner — no hay que pasarlo en --labels.
  • Los strings de label son sensibles a mayúsculas/minúsculas y deben coincidir exactamente con los workflows: Club-12-back-runner y Club-12-front-runner.
  • Ambos runners corren en el mismo host (192.168.0.200), sobre el mismo proyecto de compose (club12) — por diseño, para que ambos jobs de deploy puedan compartir el mismo directorio de compose sin pisarse.

3. Bootstrap del .env de producción (manual, una sola vez)

El pipeline nunca crea, lee ni sobrescribe el .env de producción. Si falta, el paso de sincronización del job deploy falla de forma explícita y detiene el workflow.

mkdir -p /home/docker-compose/Club12
cp .env.example /home/docker-compose/Club12/.env
# completar cada valor CHANGE_ME en /home/docker-compose/Club12/.env
chmod 600 /home/docker-compose/Club12/.env

Este archivo vive de forma permanente en /home/docker-compose/Club12/, un directorio distinto al workspace efímero que usa actions/checkout — por eso git clean -ffdx del checkout nunca lo toca.

4. Visibilidad del paquete en GHCR

Los paquetes de GHCR son privados por defecto, incluso en un repositorio público. El job de deploy ya hace docker login ghcr.io con un token con permiso read:packages, así que el pull funciona sin cambios adicionales. Alternativa: si se prefiere evitar el login, se puede cambiar la visibilidad del paquete a público desde la UI de GHCR (Package settings → Change visibility).

5. Rollback manual

Si un deploy deja el servicio en mal estado, se puede volver a la imagen anterior directamente en el servidor, sin pasar por GitHub:

cd /home/docker-compose/Club12
docker tag ghcr.io/francoru/club12-backend:previous ghcr.io/francoru/club12-backend:latest
docker compose up -d --no-deps --no-build backend

(Reemplazar backend por frontend y el nombre de imagen correspondiente para el frontend.)

6. Desarrollo local (sin cambios)

docker-compose.yml conserva los bloques build: como fallback local — el pipeline de CI nunca los usa (--no-build en cada deploy), pero siguen funcionando para levantar el proyecto en la máquina de desarrollo:

docker compose build && docker compose up -d

En un host que no sea Linux, la ruta absoluta del bind mount del servicio db (/home/docker/club12/db) no aplica: usar un docker-compose.override.yml (ignorado por git) que reemplace esa línea de volumen por un volumen nombrado local.

7. Migración a Postgres self-hosted (manual, una sola vez)

A partir del cambio selfhosted-postgres-db, docker-compose.yml incluye un servicio db (postgres:17-alpine) en la red interna club12, sin puerto publicado. El backend deja de hablar con Supabase Postgres y pasa a usar ese contenedor. Supabase Storage (buckets de imágenes y fichas médicas) se sigue usando igual — no se toca.

El pipeline de CI no levanta db (deploy-backend.yml usa --no-deps). El servicio se levanta una vez en el cutover y se mantiene solo por restart: unless-stopped.

7.1 Preparar el host

# Directorio de datos de Postgres — en /home (420 GB), NO en la partición raíz (31 GB)
sudo mkdir -p /home/docker/club12/db
sudo chown 999:999 /home/docker/club12/db      # uid del usuario postgres en la imagen alpine

# Directorio de backups
sudo mkdir -p /home/docker/backups/club12

7.2 Completar el .env de producción

En /home/docker-compose/Club12/.env (owner gh-runner:gh-runner, mode 600) agregar las tres claves nuevas y reescribir la connection string. Elegir la contraseña ahora (openssl rand -base64 24); Username/Password de la connection string deben coincidir con POSTGRES_USER/POSTGRES_PASSWORD.

POSTGRES_USER=postgres
POSTGRES_DB=postgres
POSTGRES_PASSWORD=<contraseña-fuerte-elegida-ahora>

# reemplaza el valor anterior (pooler de Supabase):
ConnectionStrings__DbConnection=Host=db;Port=5432;Database=postgres;Username=postgres;Password=<misma-contraseña>;SSL Mode=Disable

# activa el backup periódico + restore-desde-panel ya incluido en la app
# (reemplaza al cron manual — ver §7.5):
Backup__Enabled=true

Las variables POSTGRES_* sólo inicializan la base en el primer arranque del contenedor (directorio de datos vacío). Después, para cambiar la contraseña hay que hacerlo con ALTER USER dentro de la base.

7.3 Dump fresco de Supabase

Desde el host, usando postgres:17-alpine como cliente (no depende de la versión de pg_dump del host). Los datos del app viven en dos schemas: public (Identity / AspNet*) y Club12 (todo lo demás). Pasá las credenciales como variables de entorno para no pelear con el URL-encoding de la contraseña; sacá los valores del .env actual (grep -i DbConnection /home/docker-compose/Club12/.env) — User Id= → PGUSER, Password= → PGPASSWORD, Server= → PGHOST.

set +H                          # bash: desactiva la expansión de '!' en la password
cd /home/docker/backups/club12

docker run --rm -v "$PWD:/out" \
  -e PGHOST=aws-1-us-east-2.pooler.supabase.com -e PGPORT=5432 -e PGDATABASE=postgres \
  -e PGUSER='<User Id>' -e PGPASSWORD='<Password>' -e PGSSLMODE=require \
  postgres:17-alpine \
  pg_dump --format=custom --no-owner --no-privileges \
  --schema=public --schema='"Club12"' \
  --file=/out/club12-cutover-$(date +%F).dump

--schema='"Club12"' con comillas dobles adentro: pg_dump pasa los patrones de --schema a minúsculas, así que --schema=Club12 no matchea nada y el dump sale con solo la mitad de las tablas.

Verificá que estén los dos schemas:

docker run --rm -v "$PWD:/out" postgres:17-alpine \
  pg_restore --list "/out/club12-cutover-$(date +%F).dump" | grep "TABLE DATA"

Tenés que ver ~22 tablas en Club12 (Teams, Players, Tournaments, Matches, …) y 8 en public (AspNetUsers, __EFMigrationsHistory, …).

7.4 Cutover (ventana corta — el backend se reinicia)

cd /home/docker-compose/Club12
DUMP=/home/docker/backups/club12/club12-cutover-$(date +%F).dump

# 1. Levantar SOLO la base y esperar a que esté healthy
docker compose up -d db
docker compose ps db                       # STATUS = healthy

# 2. Restaurar el dump dentro del contenedor
docker compose cp "$DUMP" db:/tmp/restore.dump
docker compose exec -T db pg_restore --no-owner --no-privileges -U postgres -d postgres /tmp/restore.dump
docker compose exec -T db rm /tmp/restore.dump
#    El único error esperado es `schema "public" already exists` (1, ignorado) — benigno.
#    Cualquier otro error rojo: frená y revisá antes de seguir.

# 3. Verificación
docker compose exec -T db psql -U postgres -d postgres -c '\dt "Club12".*'
docker compose exec -T db psql -U postgres -d postgres -c 'select count(*) from "Club12"."Teams";'
docker compose exec -T db psql -U postgres -d postgres -c 'select count(*) from "public"."AspNetUsers";'

# 4. Con el .env ya editado (7.2 — cambiar ConnectionStrings__DbConnection a Host=db;…),
#    recrear el backend
docker compose up -d backend
docker compose logs --tail=40 backend      # sin errores de migración, "Now listening on: http://[::]:8080"

# 5. Bouncear el frontend: su nginx tiene cacheada la IP vieja del backend → si no,
#    todo /api/* da 502 hasta reiniciarlo.
docker compose restart frontend

# 6. Verificar (el backend no está publicado en el host — va por el proxy :5001)
docker compose ps
curl -s -o /dev/null -w 'http=%{http_code}\n' 'http://localhost:5001/api/tournaments?pageSize=300'

En el arranque, MigrateAsync ve el schema ya restaurado y no hace nada (o aplica sólo migraciones más nuevas que el dump). El seed está en Seed:Enabled=false en producción.

El paso 5 (restart frontend) queda automatizado en deploy-backend.yml para los deploys de CI; en el cutover manual hay que hacerlo a mano.

7.5 Backup automático (feature integrado del backend)

El backend ya trae un sistema de backups completo — no hace falta cron ni script en el host. Se activa con la variable agregada en §7.2:

Backup__Enabled=true
# opcionales, si se quiere otra cadencia (defaults entre paréntesis):
# Backup__IntervalHours=24      (24)
# Backup__RetentionCount=7      (7)

Con Backup__Enabled=true, un DatabaseBackupHostedService corre dentro del propio proceso backend cada Backup__IntervalHours horas, genera el dump con pg_dump (ya instalado en la imagen del backend) y lo guarda en el volumen backup-data (Backup__LocalStoragePath, default /app/backups), podando al Backup__RetentionCount más reciente.

  • Ver los backups: panel admin → sección de backups. Cada fila muestra su origen ("Programado" para los automáticos, "Manual" para los que se disparan a mano desde el mismo panel).
  • Restaurar: botón de restore en el panel (POST /api/backups/{id}/restore) — sin SSH al servidor. Antes de restaurar, la app crea automáticamente un backup de seguridad del estado actual.
  • Verificar la integridad de un dump sin arriesgar producción: descargarlo desde el panel y restaurarlo en una base "scratch" separada (no usar el botón de restore de producción solo para testear que un dump sirve).

7.6 Rollback

# En el .env, volver ConnectionStrings__DbConnection al valor del pooler de Supabase
docker compose up -d backend

No hay migración de schema que revertir. Dejar el contenedor db y /home/docker/club12/db en su lugar hasta confirmar que el cutover quedó estable; recién ahí dar de baja el proyecto de Supabase (los buckets de Storage siguen en uso).

7.7 docker compose down — cuidado

Los datos de db son un bind mount: sobreviven down y down -v. Aun así, -v elimina los volúmenes nombrados (backup-data), y borrar /home/docker/club12/db a mano destruye la base.

Checklist post-merge

Estos pasos no están cubiertos por el cambio de código en sí — son responsabilidad del usuario/operador una vez que este cambio se mergea a develop, y deben verificarse a mano porque ningún runner self-hosted existe todavía en este momento:

  • Registrar ambos runners con las labels exactas Club-12-back-runner y Club-12-front-runner (sección 2).
  • Crear el .env real en /home/docker-compose/Club12/.env a partir de .env.example (sección 3).
  • Hacer un push chico a develop que solo toque Club12-Backend/** y confirmar que:
    • solo corre deploy-backend.yml (no deploy-frontend.yml);
    • el job build publica ghcr.io/francoru/club12-backend:latest;
    • el job deploy corre después de que build termina, en el runner Club-12-back-runner;
    • docker images muestra club12-backend:previous (o el mensaje de "primer deploy" si es la primera vez);
    • docker compose ps backend muestra el contenedor recreado y saludable;
    • el contenedor de frontend no se reinicia.
  • Repetir el mismo push de prueba tocando solo Club12-WebClient/** y confirmar el mismo comportamiento en espejo para deploy-frontend.yml.
  • Confirmar que /home/docker-compose/Club12/.env sigue existiendo después del deploy y que el servicio toma sus valores (por ejemplo, que la API arranca sin errores de configuración).
  • Confirmar que docker image prune -f no deja crecer una pila de imágenes colgantes, y que las tags :latest y :previous sobreviven.