Sube documentacion del proyecto
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
# 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 <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:
|
||||
|
||||
```bash
|
||||
./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.
|
||||
|
||||
```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=<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`.
|
||||
|
||||
```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='<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:
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user