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 versiondebe responderv2.x). -
El usuario que corre el runner (
ghrunner) debe pertenecer al grupodocker:sudo usermod -aG docker ghrunner # cerrar sesión y volver a entrar (o `newgrp docker`) para que el cambio de grupo tome efectoVerificar como ese usuario, sin
sudo:docker psSi 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-hostedse 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-runneryClub-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 dedeploypuedan 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 conALTER USERdentro 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_dumppasa los patrones de--schemaa minúsculas, así que--schema=Club12no 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 endeploy-backend.ymlpara 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-runneryClub-12-front-runner(sección 2). - Crear el
.envreal en/home/docker-compose/Club12/.enva partir de.env.example(sección 3). - Hacer un push chico a
developque solo toqueClub12-Backend/**y confirmar que:- solo corre
deploy-backend.yml(nodeploy-frontend.yml); - el job
buildpublicaghcr.io/francoru/club12-backend:latest; - el job
deploycorre después de quebuildtermina, en el runnerClub-12-back-runner; docker imagesmuestraclub12-backend:previous(o el mensaje de "primer deploy" si es la primera vez);docker compose ps backendmuestra el contenedor recreado y saludable;- el contenedor de
frontendno se reinicia.
- solo corre
- Repetir el mismo push de prueba tocando solo
Club12-WebClient/**y confirmar el mismo comportamiento en espejo paradeploy-frontend.yml. - Confirmar que
/home/docker-compose/Club12/.envsigue 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 -fno deja crecer una pila de imágenes colgantes, y que las tags:latesty:previoussobreviven.