# 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`: ```bash 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`: ```bash 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): ```bash ./config.sh --url https://github.com/FrancoRu/Taller-de-Integracion \ --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: ```bash ./config.sh --url https://github.com/FrancoRu/Taller-de-Integracion \ --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. ```bash 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: ```bash 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: ```bash 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 ```bash # 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`. ```bash POSTGRES_USER=postgres POSTGRES_DB=postgres POSTGRES_PASSWORD= # reemplaza el valor anterior (pooler de Supabase): ConnectionStrings__DbConnection=Host=db;Port=5432;Database=postgres;Username=postgres;Password=;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`. ```bash 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='' -e PGPASSWORD='' -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: ```bash 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) ```bash 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: ```bash 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 ```bash # 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.