From a45675b6a61979fce4bf111ec0f92ff8d524f0fb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 3 Sep 2026 04:54:15 +0000 Subject: [PATCH] =?UTF-8?q?deploy:=20Liquibase=20autom=C3=A1tico=20en=20ca?= =?UTF-8?q?da=20deploy=20(job=20migrate)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Coolify usa docker-compose.yml: migrate aplica schema (!dev) y la API espera a que termine. Postgres/Redis locales quedan en docker-compose.local.yml. Co-authored-by: alberto.martinez --- Dockerfile.migrate | 2 +- db/README.md | 6 ++- docker-compose.local.yml | 80 ++++++++++++++++++++++++++++ docker-compose.yml | 109 +++++++++------------------------------ docs/coolify.md | 31 +++++------ package.json | 4 +- 6 files changed, 125 insertions(+), 107 deletions(-) create mode 100644 docker-compose.local.yml diff --git a/Dockerfile.migrate b/Dockerfile.migrate index d258f5c..bc0cf0a 100644 --- a/Dockerfile.migrate +++ b/Dockerfile.migrate @@ -14,4 +14,4 @@ COPY db/ ./db/ RUN chmod +x ./db/bootstrap-tools.sh ./db/update.sh && ./db/bootstrap-tools.sh ENTRYPOINT ["./db/update.sh"] -CMD ["all", "--context-filter=dev"] +CMD ["all", "--context-filter=!dev"] diff --git a/db/README.md b/db/README.md index 6d82773..c8cb95f 100644 --- a/db/README.md +++ b/db/README.md @@ -26,8 +26,10 @@ que el aislamiento realmente se cumple. ## Aplicar migraciones -Requiere Java 17+ y las credenciales del rol `_owner` de cada módulo (nunca -`_app`, que es solo runtime): +En **Coolify** no se corre a mano: el servicio `migrate` de `docker-compose.yml` +aplica `all --context-filter=!dev` en cada deploy, antes de levantar `api`. + +A mano (Java 17+ y credenciales `_owner`, nunca `_app`): ```bash # Local, después de correr db/provision/dev-local.sh: diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..09eb1f4 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,80 @@ +# Desarrollo local: Postgres + Redis en compose, provision y seed `dev`. +# Uso: docker compose -f docker-compose.yml -f docker-compose.local.yml up --build +services: + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:?define POSTGRES_SUPERUSER_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + expose: + - "5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 10 + + redis: + image: redis:7-alpine + restart: unless-stopped + expose: + - "6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 10 + + provision: + build: + context: . + dockerfile: Dockerfile.provision + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + PGHOST: postgres + PGPASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:?} + PLATFORM_OWNER_PASSWORD: ${PLATFORM_OWNER_PASSWORD:?} + PLATFORM_APP_PASSWORD: ${PLATFORM_APP_PASSWORD:?} + IAM_OWNER_PASSWORD: ${IAM_OWNER_PASSWORD:?} + IAM_APP_PASSWORD: ${IAM_APP_PASSWORD:?} + CORE_OWNER_PASSWORD: ${CORE_OWNER_PASSWORD:?} + CORE_APP_PASSWORD: ${CORE_APP_PASSWORD:?} + REDIS_ADMIN_URL: redis://redis:6379 + IAM_REDIS_PASSWORD: ${IAM_REDIS_PASSWORD:?} + CORE_REDIS_PASSWORD: ${CORE_REDIS_PASSWORD:?} + command: + - -c + - "./db/provision/docker-provision.sh && ./db/provision/05-redis-acl.sh" + + migrate: + depends_on: + provision: + condition: service_completed_successfully + environment: + DATABASE_URL_PLATFORM_OWNER: postgresql://panels_platform_owner:${PLATFORM_OWNER_PASSWORD:?}@postgres:5432/panels_platform + DATABASE_URL_IAM_OWNER: postgresql://panels_iam_owner:${IAM_OWNER_PASSWORD:?}@postgres:5432/panels_product + DATABASE_URL_CORE_OWNER: postgresql://panels_core_owner:${CORE_OWNER_PASSWORD:?}@postgres:5432/panels_product + command: ["all", "--context-filter=dev"] + + api: + depends_on: + redis: + condition: service_healthy + environment: + DENO_ENV: development + DATABASE_URL_PLATFORM: postgresql://panels_platform_app:${PLATFORM_APP_PASSWORD:?}@postgres:5432/panels_platform + DATABASE_URL_IAM: postgresql://panels_iam_app:${IAM_APP_PASSWORD:?}@postgres:5432/panels_product + DATABASE_URL_CORE: postgresql://panels_core_app:${CORE_APP_PASSWORD:?}@postgres:5432/panels_product + DATABASE_URL_IAM_OWNER: postgresql://panels_iam_owner:${IAM_OWNER_PASSWORD:?}@postgres:5432/panels_product + DATABASE_URL_CORE_OWNER: postgresql://panels_core_owner:${CORE_OWNER_PASSWORD:?}@postgres:5432/panels_product + REDIS_URL_IAM: redis://panels_iam_redis:${IAM_REDIS_PASSWORD:?}@redis:6379 + REDIS_URL_CORE: redis://panels_core_redis:${CORE_REDIS_PASSWORD:?}@redis:6379 + +volumes: + postgres-data: diff --git a/docker-compose.yml b/docker-compose.yml index 62fd415..0d69c5e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,74 +1,19 @@ -# Coolify / local — Postgres + Redis (ver plan de migración: monolito -# modular panels_platform / panels_product con esquemas iam+core). -# En Coolify, postgres/redis normalmente son recursos gestionados aparte -# (ver docs/coolify.md); este compose incluye contenedores propios para -# que `docker compose up` funcione standalone en desarrollo local. +# Coolify / producción — Postgres y Redis son recursos gestionados aparte. +# Liquibase corre solo en el job `migrate` (un shot por deploy); la API espera +# a que termine. No arranca Java dentro del contenedor de tráfico. +# +# Local: docker compose -f docker-compose.yml -f docker-compose.local.yml up --build services: - postgres: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:?define POSTGRES_SUPERUSER_PASSWORD} - volumes: - - postgres-data:/var/lib/postgresql/data - expose: - - "5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 10 - - redis: - image: redis:7-alpine - restart: unless-stopped - expose: - - "6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 10 - - # Jobs de un solo uso (nunca en producción -- ver db/provision/README.md): - # 1) crea roles/bases/esquemas/ACLs, 2) aplica Liquibase. `api` espera a - # que ambos terminen bien antes de arrancar. - provision: - build: - context: . - dockerfile: Dockerfile.provision - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - environment: - PGHOST: postgres - PGPASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:?} - PLATFORM_OWNER_PASSWORD: ${PLATFORM_OWNER_PASSWORD:?} - PLATFORM_APP_PASSWORD: ${PLATFORM_APP_PASSWORD:?} - IAM_OWNER_PASSWORD: ${IAM_OWNER_PASSWORD:?} - IAM_APP_PASSWORD: ${IAM_APP_PASSWORD:?} - CORE_OWNER_PASSWORD: ${CORE_OWNER_PASSWORD:?} - CORE_APP_PASSWORD: ${CORE_APP_PASSWORD:?} - REDIS_ADMIN_URL: redis://redis:6379 - IAM_REDIS_PASSWORD: ${IAM_REDIS_PASSWORD:?} - CORE_REDIS_PASSWORD: ${CORE_REDIS_PASSWORD:?} - command: - - -c - - "./db/provision/docker-provision.sh && ./db/provision/05-redis-acl.sh" - migrate: build: context: . dockerfile: Dockerfile.migrate - depends_on: - provision: - condition: service_completed_successfully + restart: "no" environment: - DATABASE_URL_PLATFORM_OWNER: postgresql://panels_platform_owner:${PLATFORM_OWNER_PASSWORD:?}@postgres:5432/panels_platform - DATABASE_URL_IAM_OWNER: postgresql://panels_iam_owner:${IAM_OWNER_PASSWORD:?}@postgres:5432/panels_product - DATABASE_URL_CORE_OWNER: postgresql://panels_core_owner:${CORE_OWNER_PASSWORD:?}@postgres:5432/panels_product + DATABASE_URL_PLATFORM_OWNER: ${DATABASE_URL_PLATFORM_OWNER:?} + DATABASE_URL_IAM_OWNER: ${DATABASE_URL_IAM_OWNER:?} + DATABASE_URL_CORE_OWNER: ${DATABASE_URL_CORE_OWNER:?} + command: ["all", "--context-filter=!dev"] api: build: @@ -78,15 +23,14 @@ services: depends_on: migrate: condition: service_completed_successfully - redis: - condition: service_healthy environment: + DENO_ENV: ${DENO_ENV:-production} PORT: ${PORT:-8000} - SESSION_SECRET: ${SESSION_SECRET} - DOCS_KEY: ${DOCS_KEY} - SEED_PASSWORD: ${SEED_PASSWORD} + SESSION_SECRET: ${SESSION_SECRET:?} + DOCS_KEY: ${DOCS_KEY:?} + SEED_PASSWORD: ${SEED_PASSWORD:-} API_KEY: ${API_KEY:-} - PANEL_LOGIN_URL: ${PANEL_LOGIN_URL} + PANEL_LOGIN_URL: ${PANEL_LOGIN_URL:?} COOKIE_SECURE: ${COOKIE_SECURE:-true} CORS_ORIGINS: ${CORS_ORIGINS:-} VCARD_BASE: ${VCARD_BASE:-} @@ -95,21 +39,17 @@ services: SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} SMTP_FROM: ${SMTP_FROM:-} - # Postgres: panels_platform (base separada) + panels_product (iam/core) - DATABASE_URL_PLATFORM: postgresql://panels_platform_app:${PLATFORM_APP_PASSWORD:?}@postgres:5432/panels_platform - DATABASE_URL_IAM: postgresql://panels_iam_app:${IAM_APP_PASSWORD:?}@postgres:5432/panels_product - DATABASE_URL_CORE: postgresql://panels_core_app:${CORE_APP_PASSWORD:?}@postgres:5432/panels_product - DATABASE_URL_IAM_OWNER: postgresql://panels_iam_owner:${IAM_OWNER_PASSWORD:?}@postgres:5432/panels_product - DATABASE_URL_CORE_OWNER: postgresql://panels_core_owner:${CORE_OWNER_PASSWORD:?}@postgres:5432/panels_product - # Redis: ACLs por módulo (ver db/provision/05-redis-acl.sh) - REDIS_URL_IAM: redis://panels_iam_redis:${IAM_REDIS_PASSWORD:?}@redis:6379 - REDIS_URL_CORE: redis://panels_core_redis:${CORE_REDIS_PASSWORD:?}@redis:6379 - # Contabo Object Storage (Fase 4c) -- opcional; sin esto cae a disco - # local bajo el volumen panel-data (no recomendado en producción, - # ver plan: un volumen local no escala horizontalmente). + DATABASE_URL_PLATFORM: ${DATABASE_URL_PLATFORM:?} + DATABASE_URL_PLATFORM_OWNER: ${DATABASE_URL_PLATFORM_OWNER:-} + DATABASE_URL_IAM: ${DATABASE_URL_IAM:?} + DATABASE_URL_IAM_OWNER: ${DATABASE_URL_IAM_OWNER:?} + DATABASE_URL_CORE: ${DATABASE_URL_CORE:?} + DATABASE_URL_CORE_OWNER: ${DATABASE_URL_CORE_OWNER:?} + REDIS_URL_IAM: ${REDIS_URL_IAM:?} + REDIS_URL_CORE: ${REDIS_URL_CORE:?} S3_ENDPOINT: ${S3_ENDPOINT:-} S3_BUCKET: ${S3_BUCKET:-} - S3_REGION: ${S3_REGION:-} + S3_REGION: ${S3_REGION:-auto} S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-} S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-} volumes: @@ -156,5 +96,4 @@ services: - "80" volumes: - postgres-data: panel-data: diff --git a/docs/coolify.md b/docs/coolify.md index 5a7d17d..23074c6 100644 --- a/docs/coolify.md +++ b/docs/coolify.md @@ -7,7 +7,7 @@ | Postgres gestionado en Coolify | **1 instancia** por ambiente, con **2 bases**: `panels_platform`, `panels_product` (esquemas `iam`/`core`) | | Redis gestionado en Coolify | **1 instancia** por ambiente, con **2 usuarios ACL** (`panels_iam_redis`, `panels_core_redis`) | | Object storage | Cloudflare R2 (S3-compatible) para expedientes/PDFs/logos cifrados | -| Contenedores de la app | **3** (`api`, `web-panel`, `web-saas`) | +| Contenedores de la app | **4** (`migrate` un shot por deploy + `api` + `web-panel` + `web-saas`) | | Volumen persistente | **1** → `/app/data` en `api` (solo fallback local si no hay Contabo configurado -- no usar así en producción) | Los fronts (Alpine + nginx) hacen proxy de `/v1` al servicio `api`, así las cookies de sesión van same-origin. @@ -36,6 +36,7 @@ distintos dentro de `panels_product`, y las reglas del monolito modular. | `SEED_PASSWORD` | string | Password inicial usado por `api/scripts/bootstrap-admin.ts` | | `PANEL_LOGIN_URL` | URL absoluta | Link en correos de acceso | | `DATABASE_URL_PLATFORM` | `postgresql://panels_platform_app:...@host:5432/panels_platform` | Runtime, rol `_app` | +| `DATABASE_URL_PLATFORM_OWNER` | igual, rol `_owner` | Job `migrate` (Liquibase SaaS) | | `DATABASE_URL_IAM` | `postgresql://panels_iam_app:...@host:5432/panels_product` | Runtime, rol `_app`, esquema `iam` | | `DATABASE_URL_CORE` | `postgresql://panels_core_app:...@host:5432/panels_product` | Runtime, rol `_app`, esquema `core` | | `DATABASE_URL_IAM_OWNER` | igual, rol `_owner` | Solo para el lookup de login por username (bypassa RLS a propósito, ver `api/iam_db.ts`) | @@ -83,29 +84,25 @@ En Coolify el servicio de PANELS se llama **`web-panel`** (FQDN ej. `panels.mrde 1. **Provisionar Postgres, Redis y bucket** — 1 de cada **por ambiente** (no por módulo; no compartir prod con staging). 2. **Crear accesos y validar**: `./db/provision/create-accesses.sh --apply --verify --out .env..local` contra esos hosts (ver [`db/provision/README.md`](../db/provision/README.md)). Coolify no crea los roles `panels_*` ni los ACL de Redis solo. -3. **Aplicar Liquibase** (paso explícito, NO ocurre al arrancar la app): `./db/update.sh all --context-filter='!dev'` con las credenciales `_owner`. En staging/producción, **nunca** olvidar el `--context-filter` -- sin él, Liquibase corre TAMBIÉN los changesets de demo (`context=dev`). -4. **Bootstrap del primer admin**: `deno run ... api/scripts/bootstrap-admin.ts platform` y `... tenant --tenant-id=... --company-code=...` (ver `db/README.md`). -5. **Push** este repo (sin `.env` ni `data/`). -6. Coolify → **New Resource → Docker Compose** → `docker-compose.yml` (solo para `api`/`web-panel`/`web-saas` -- si Postgres/Redis ya son recursos gestionados aparte, quitar esos servicios del compose antes de desplegar, o apuntar sus variables a los recursos gestionados en vez de los contenedores locales del compose). -7. **Persistent storage:** volumen `panel-data` → `/app/data` en `api` (solo fallback de archivos si no hay Contabo). +3. **Coolify → Docker Compose** → `docker-compose.yml` (este archivo **no** levanta Postgres/Redis; usa los recursos que ya creaste). Conecta el stack a la **misma red** que Postgres y Redis (Connect to Predefined Network). +4. Cargar en el recurso compose **todas** las variables obligatorias (incluidas las 3 `DATABASE_URL_*_OWNER`: las usa el job `migrate`). `DENO_ENV=production`. +5. **Deploy.** En cada deploy corre `migrate` (`--context-filter=!dev`, sin demo) y **después** arranca `api`. No hace falta entrar al VPS a correr Liquibase. +6. **Bootstrap del primer admin** (solo la primera vez): `bootstrap-admin.ts` (ver `db/README.md`). +7. **Persistent storage:** volumen `panel-data` → `/app/data` en `api` (solo fallback si no hay R2). 8. **Dominios:** `web-panel` → panels; `web-saas` → saas; `api` sin FQDN (proxy `/v1`). -9. Cargar en Coolify todas las variables **obligatorias** de la tabla de arriba + `COOKIE_SECURE=true`. -10. Deploy. -11. Verificar `https://app…/v1/health` → `ok`, `core`, `platform`, `redis.iam`, `redis.core` y `storage` (en prod `backend:"s3"`). Si algo falla, la API no arranca. -12. Login SaaS `admin` / tu `SEED_PASSWORD`. -13. SMTP en `/smtp` o por `SMTP_*`. +9. Verificar `https://app…/v1/health` → `ok`, `core`, `platform`, `redis.iam`, `redis.core` y `storage` (en prod `backend:"s3"`). Si algo falla, la API no arranca. +10. Login SaaS `admin` / tu `SEED_PASSWORD` (tras el bootstrap). +11. SMTP en `/smtp` o por `SMTP_*`. ## Local (todo en docker-compose, incluyendo Postgres/Redis propios) ```bash -cp .env.example .env # rellenar TODAS las variables (incluye los passwords de roles Postgres/Redis) -docker compose up --build +cp .env.example .env +docker compose -f docker-compose.yml -f docker-compose.local.yml up --build ``` -El compose local incluye: `provision` (roles/esquemas/ACLs, un solo uso) → -`migrate` (Liquibase con datos de demo, un solo uso) → `api`/`web-panel`/`web-saas`. -Para producción, `provision`/`migrate` NO se corren así -- ver -`db/provision/README.md` y `db/RUNBOOK-corte.md`. +El overlay local añade Postgres/Redis + `provision` y corre Liquibase con `context=dev` (demo). +En Coolify solo se usa `docker-compose.yml`: `migrate` con `!dev`. Sin las variables obligatorias, compose **no arranca** (`:?` en docker-compose.yml). diff --git a/package.json b/package.json index b5334d8..a5cc433 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev:web": "cd web-panel && npm run dev", "dev:saas": "cd web-saas && npm run dev", "db:migrate": "./db/update.sh all", - "docker:build": "docker compose build", - "docker:up": "docker compose up -d" + "docker:build": "docker compose -f docker-compose.yml -f docker-compose.local.yml build", + "docker:up": "docker compose -f docker-compose.yml -f docker-compose.local.yml up -d" } }