Compare commits

..

1 commit

Author SHA1 Message Date
Cursor Origin
08442f9c9c Pre-computed merge for change #1 1970-01-01 00:00:00 +00:00
8 changed files with 119 additions and 138 deletions

View file

@ -66,12 +66,12 @@ IAM_REDIS_PASSWORD=
CORE_REDIS_PASSWORD= CORE_REDIS_PASSWORD=
# ============================================================================= # =============================================================================
# Object storage S3-compatible (Cloudflare R2) -- expedientes/PDFs/logos. # Contabo Object Storage (Fase 4c) -- expedientes/PDFs/logos cifrados.
# Sin esto, cae a disco local (solo dev). # Sin esto, cae a disco local bajo el volumen panel-data (solo dev; en
# R2: S3_ENDPOINT=https://<ACCOUNT_ID>.r2.cloudflarestorage.com S3_REGION=auto # producción un volumen local no escala horizontalmente, ver el plan).
# ============================================================================= # =============================================================================
S3_ENDPOINT= S3_ENDPOINT=
S3_BUCKET= S3_BUCKET=
S3_REGION=auto S3_REGION=
S3_ACCESS_KEY_ID= S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY= S3_SECRET_ACCESS_KEY=

View file

@ -14,4 +14,4 @@ COPY db/ ./db/
RUN chmod +x ./db/bootstrap-tools.sh ./db/update.sh && ./db/bootstrap-tools.sh RUN chmod +x ./db/bootstrap-tools.sh ./db/update.sh && ./db/bootstrap-tools.sh
ENTRYPOINT ["./db/update.sh"] ENTRYPOINT ["./db/update.sh"]
CMD ["all", "--context-filter=!dev"] CMD ["all", "--context-filter=dev"]

View file

@ -17,9 +17,11 @@ import { config, DATA_DIR } from "./config.ts";
* otra capa. El contenido YA viene cifrado con AES-GCM (docs_crypto.ts) * otra capa. El contenido YA viene cifrado con AES-GCM (docs_crypto.ts)
* antes de llegar aquí -- el bucket nunca ve texto plano. * antes de llegar aquí -- el bucket nunca ve texto plano.
* *
* Backend real: S3-compatible (Cloudflare R2). Si no hay credenciales S3 * Backend real: Contabo Object Storage (S3-compatible). Si no hay
* (dev local), cae a disco bajo DATA_DIR. En staging/producción el volumen * credenciales S3 configuradas (dev local sin Contabo), cae a disco local
* local no escala: hay que configurar S3_*. * bajo DATA_DIR -- útil para desarrollar/probar sin depender de Contabo,
* pero NO es la ruta recomendada para staging/producción (ver Fase 4c del
* plan: un volumen local no escala horizontalmente).
*/ */
let client: S3Client | null = null; let client: S3Client | null = null;
@ -70,11 +72,8 @@ function getClient(): S3Client {
if (!client) { if (!client) {
client = new S3Client({ client = new S3Client({
endpoint: config.s3Endpoint, endpoint: config.s3Endpoint,
region: config.s3Region || "auto", region: config.s3Region,
forcePathStyle: true, forcePathStyle: true,
// R2 no implementa los checksums CRC32 que el SDK v3 manda por defecto.
requestChecksumCalculation: "WHEN_REQUIRED",
responseChecksumValidation: "WHEN_REQUIRED",
credentials: { credentials: {
accessKeyId: config.s3AccessKeyId, accessKeyId: config.s3AccessKeyId,
secretAccessKey: config.s3SecretAccessKey, secretAccessKey: config.s3SecretAccessKey,

View file

@ -26,10 +26,8 @@ que el aislamiento realmente se cumple.
## Aplicar migraciones ## Aplicar migraciones
En **Coolify** no se corre a mano: el servicio `migrate` de `docker-compose.yml` Requiere Java 17+ y las credenciales del rol `_owner` de cada módulo (nunca
aplica `all --context-filter=!dev` en cada deploy, antes de levantar `api`. `_app`, que es solo runtime):
A mano (Java 17+ y credenciales `_owner`, nunca `_app`):
```bash ```bash
# Local, después de correr db/provision/dev-local.sh: # Local, después de correr db/provision/dev-local.sh:

View file

@ -1,80 +0,0 @@
# 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:

View file

@ -1,19 +1,74 @@
# Coolify / producción — Postgres y Redis son recursos gestionados aparte. # Coolify / local — Postgres + Redis (ver plan de migración: monolito
# Liquibase corre solo en el job `migrate` (un shot por deploy); la API espera # modular panels_platform / panels_product con esquemas iam+core).
# a que termine. No arranca Java dentro del contenedor de tráfico. # En Coolify, postgres/redis normalmente son recursos gestionados aparte
# # (ver docs/coolify.md); este compose incluye contenedores propios para
# Local: docker compose -f docker-compose.yml -f docker-compose.local.yml up --build # que `docker compose up` funcione standalone en desarrollo local.
services: 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: migrate:
build: build:
context: . context: .
dockerfile: Dockerfile.migrate dockerfile: Dockerfile.migrate
restart: "no" depends_on:
provision:
condition: service_completed_successfully
environment: environment:
DATABASE_URL_PLATFORM_OWNER: ${DATABASE_URL_PLATFORM_OWNER:?} DATABASE_URL_PLATFORM_OWNER: postgresql://panels_platform_owner:${PLATFORM_OWNER_PASSWORD:?}@postgres:5432/panels_platform
DATABASE_URL_IAM_OWNER: ${DATABASE_URL_IAM_OWNER:?} DATABASE_URL_IAM_OWNER: postgresql://panels_iam_owner:${IAM_OWNER_PASSWORD:?}@postgres:5432/panels_product
DATABASE_URL_CORE_OWNER: ${DATABASE_URL_CORE_OWNER:?} DATABASE_URL_CORE_OWNER: postgresql://panels_core_owner:${CORE_OWNER_PASSWORD:?}@postgres:5432/panels_product
command: ["all", "--context-filter=!dev"]
api: api:
build: build:
@ -23,14 +78,15 @@ services:
depends_on: depends_on:
migrate: migrate:
condition: service_completed_successfully condition: service_completed_successfully
redis:
condition: service_healthy
environment: environment:
DENO_ENV: ${DENO_ENV:-production}
PORT: ${PORT:-8000} PORT: ${PORT:-8000}
SESSION_SECRET: ${SESSION_SECRET:?} SESSION_SECRET: ${SESSION_SECRET}
DOCS_KEY: ${DOCS_KEY:?} DOCS_KEY: ${DOCS_KEY}
SEED_PASSWORD: ${SEED_PASSWORD:-} SEED_PASSWORD: ${SEED_PASSWORD}
API_KEY: ${API_KEY:-} API_KEY: ${API_KEY:-}
PANEL_LOGIN_URL: ${PANEL_LOGIN_URL:?} PANEL_LOGIN_URL: ${PANEL_LOGIN_URL}
COOKIE_SECURE: ${COOKIE_SECURE:-true} COOKIE_SECURE: ${COOKIE_SECURE:-true}
CORS_ORIGINS: ${CORS_ORIGINS:-} CORS_ORIGINS: ${CORS_ORIGINS:-}
VCARD_BASE: ${VCARD_BASE:-} VCARD_BASE: ${VCARD_BASE:-}
@ -39,17 +95,21 @@ services:
SMTP_USER: ${SMTP_USER:-} SMTP_USER: ${SMTP_USER:-}
SMTP_PASS: ${SMTP_PASS:-} SMTP_PASS: ${SMTP_PASS:-}
SMTP_FROM: ${SMTP_FROM:-} SMTP_FROM: ${SMTP_FROM:-}
DATABASE_URL_PLATFORM: ${DATABASE_URL_PLATFORM:?} # Postgres: panels_platform (base separada) + panels_product (iam/core)
DATABASE_URL_PLATFORM_OWNER: ${DATABASE_URL_PLATFORM_OWNER:-} DATABASE_URL_PLATFORM: postgresql://panels_platform_app:${PLATFORM_APP_PASSWORD:?}@postgres:5432/panels_platform
DATABASE_URL_IAM: ${DATABASE_URL_IAM:?} DATABASE_URL_IAM: postgresql://panels_iam_app:${IAM_APP_PASSWORD:?}@postgres:5432/panels_product
DATABASE_URL_IAM_OWNER: ${DATABASE_URL_IAM_OWNER:?} DATABASE_URL_CORE: postgresql://panels_core_app:${CORE_APP_PASSWORD:?}@postgres:5432/panels_product
DATABASE_URL_CORE: ${DATABASE_URL_CORE:?} DATABASE_URL_IAM_OWNER: postgresql://panels_iam_owner:${IAM_OWNER_PASSWORD:?}@postgres:5432/panels_product
DATABASE_URL_CORE_OWNER: ${DATABASE_URL_CORE_OWNER:?} DATABASE_URL_CORE_OWNER: postgresql://panels_core_owner:${CORE_OWNER_PASSWORD:?}@postgres:5432/panels_product
REDIS_URL_IAM: ${REDIS_URL_IAM:?} # Redis: ACLs por módulo (ver db/provision/05-redis-acl.sh)
REDIS_URL_CORE: ${REDIS_URL_CORE:?} 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).
S3_ENDPOINT: ${S3_ENDPOINT:-} S3_ENDPOINT: ${S3_ENDPOINT:-}
S3_BUCKET: ${S3_BUCKET:-} S3_BUCKET: ${S3_BUCKET:-}
S3_REGION: ${S3_REGION:-auto} S3_REGION: ${S3_REGION:-}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-} S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-} S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
volumes: volumes:
@ -96,4 +156,5 @@ services:
- "80" - "80"
volumes: volumes:
postgres-data:
panel-data: panel-data:

View file

@ -6,8 +6,8 @@
|-----|--------| |-----|--------|
| Postgres gestionado en Coolify | **1 instancia** por ambiente, con **2 bases**: `panels_platform`, `panels_product` (esquemas `iam`/`core`) | | 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`) | | 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 | | Object storage | Contabo Object Storage (S3-compatible) para expedientes/PDFs/logos cifrados |
| Contenedores de la app | **4** (`migrate` un shot por deploy + `api` + `web-panel` + `web-saas`) | | Contenedores de la app | **3** (`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) | | 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. Los fronts (Alpine + nginx) hacen proxy de `/v1` al servicio `api`, así las cookies de sesión van same-origin.
@ -36,7 +36,6 @@ distintos dentro de `panels_product`, y las reglas del monolito modular.
| `SEED_PASSWORD` | string | Password inicial usado por `api/scripts/bootstrap-admin.ts` | | `SEED_PASSWORD` | string | Password inicial usado por `api/scripts/bootstrap-admin.ts` |
| `PANEL_LOGIN_URL` | URL absoluta | Link en correos de acceso | | `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` | `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_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_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`) | | `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`) |
@ -66,7 +65,7 @@ openssl rand -hex 24 # passwords de roles Postgres/Redis
| `API_KEY` | Auth alternativa por header `X-API-Key` **+ `X-Tenant-Id` obligatorio** (ya no ve todos los tenants, ver revisión de seguridad) | | `API_KEY` | Auth alternativa por header `X-API-Key` **+ `X-Tenant-Id` obligatorio** (ya no ve todos los tenants, ver revisión de seguridad) |
| `VCARD_BASE` | Prefijo QR/vCard gafetes | | `VCARD_BASE` | Prefijo QR/vCard gafetes |
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_FROM` | Correo por env (alternativa al panel `/smtp`) | | `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` / `SMTP_FROM` | Correo por env (alternativa al panel `/smtp`) |
| `S3_ENDPOINT` / `S3_BUCKET` / `S3_REGION` / `S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY` | Cloudflare R2. Endpoint `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`, `S3_REGION=auto`. Sin esto, cae a disco local -- **no recomendado en producción** | | `S3_ENDPOINT` / `S3_BUCKET` / `S3_REGION` / `S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY` | Contabo Object Storage (Fase 4c). Sin esto, cae a disco local -- **no recomendado en producción** |
`web-panel` y `web-saas` **no** necesitan variables de entorno en runtime (estáticos + proxy nginx). `web-panel` y `web-saas` **no** necesitan variables de entorno en runtime (estáticos + proxy nginx).
@ -84,25 +83,29 @@ 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). 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.<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. 2. **Crear accesos y validar**: `./db/provision/create-accesses.sh --apply --verify --out .env.<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. **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). 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. Cargar en el recurso compose **todas** las variables obligatorias (incluidas las 3 `DATABASE_URL_*_OWNER`: las usa el job `migrate`). `DENO_ENV=production`. 4. **Bootstrap del primer admin**: `deno run ... api/scripts/bootstrap-admin.ts platform` y `... tenant --tenant-id=... --company-code=...` (ver `db/README.md`).
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. 5. **Push** este repo (sin `.env` ni `data/`).
6. **Bootstrap del primer admin** (solo la primera vez): `bootstrap-admin.ts` (ver `db/README.md`). 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 si no hay R2). 7. **Persistent storage:** volumen `panel-data` → `/app/data` en `api` (solo fallback de archivos si no hay Contabo).
8. **Dominios:** `web-panel` → panels; `web-saas` → saas; `api` sin FQDN (proxy `/v1`). 8. **Dominios:** `web-panel` → panels; `web-saas` → saas; `api` sin FQDN (proxy `/v1`).
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. 9. Cargar en Coolify todas las variables **obligatorias** de la tabla de arriba + `COOKIE_SECURE=true`.
10. Login SaaS `admin` / tu `SEED_PASSWORD` (tras el bootstrap). 10. Deploy.
11. SMTP en `/smtp` o por `SMTP_*`. 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_*`.
## Local (todo en docker-compose, incluyendo Postgres/Redis propios) ## Local (todo en docker-compose, incluyendo Postgres/Redis propios)
```bash ```bash
cp .env.example .env cp .env.example .env # rellenar TODAS las variables (incluye los passwords de roles Postgres/Redis)
docker compose -f docker-compose.yml -f docker-compose.local.yml up --build docker compose up --build
``` ```
El overlay local añade Postgres/Redis + `provision` y corre Liquibase con `context=dev` (demo). El compose local incluye: `provision` (roles/esquemas/ACLs, un solo uso) →
En Coolify solo se usa `docker-compose.yml`: `migrate` con `!dev`. `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`.
Sin las variables obligatorias, compose **no arranca** (`:?` en docker-compose.yml). Sin las variables obligatorias, compose **no arranca** (`:?` en docker-compose.yml).

View file

@ -6,7 +6,7 @@
"dev:web": "cd web-panel && npm run dev", "dev:web": "cd web-panel && npm run dev",
"dev:saas": "cd web-saas && npm run dev", "dev:saas": "cd web-saas && npm run dev",
"db:migrate": "./db/update.sh all", "db:migrate": "./db/update.sh all",
"docker:build": "docker compose -f docker-compose.yml -f docker-compose.local.yml build", "docker:build": "docker compose build",
"docker:up": "docker compose -f docker-compose.yml -f docker-compose.local.yml up -d" "docker:up": "docker compose up -d"
} }
} }