api: etiquetar fallos de arranque (postgres, redis, s3)

Tras Liquibase, Coolify marca api unhealthy si el fail-fast tira
antes de Deno.serve. Los logs ahora dicen qué ping falló.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
This commit is contained in:
Cursor Agent 2026-09-03 05:32:06 +00:00
parent b68313c5ee
commit ccf86c5b2e
No known key found for this signature in database
2 changed files with 36 additions and 8 deletions

View file

@ -1252,22 +1252,36 @@ registerPayrollRoutes(app);
const port = config.port; const port = config.port;
function startup(name: string, p: Promise<unknown>): Promise<unknown> {
return p.then(() => {
console.log(`[startup] ok ${name}`);
}).catch((e: unknown) => {
const msg = e instanceof Error ? e.message : String(e);
console.error(`[startup] FAIL ${name}: ${msg}`);
throw e;
});
}
// Fail-fast (Fase 7): antes con SQLite la app "siempre arrancaba" (creaba // Fail-fast (Fase 7): antes con SQLite la app "siempre arrancaba" (creaba
// el archivo si no existía). Con Postgres/Redis, si alguna de las 5 // el archivo si no existía). Con Postgres/Redis, si alguna de las 5
// conexiones no responde al arrancar, es mejor fallar ruidosamente que // conexiones no responde al arrancar, es mejor fallar ruidosamente que
// dejar que el primer request autenticado descubra el problema. // dejar que el primer request autenticado descubra el problema.
await Promise.all([ await Promise.all([
pingCoreDb(), startup("postgres core (DATABASE_URL_CORE)", pingCoreDb()),
pingPlatformDb(), startup("postgres platform (DATABASE_URL_PLATFORM)", pingPlatformDb()),
pingRedis().then((r) => { startup("redis iam+core", pingRedis().then((r) => {
if (!r.iam || !r.core) throw new Error("Redis (iam/core) no responde"); if (!r.iam || !r.core) {
}), throw new Error(
pingStorage().then((s) => { `Redis no responde (iam=${r.iam} core=${r.core}). REDIS_URL_* debe usar el hostname de Redis URL (internal), no el UUID de Postgres.`,
if (!s.ok) throw new Error("Storage (Contabo o disco local) no responde"); );
}
})),
startup("storage S3/R2", pingStorage().then((s) => {
if (!s.ok) throw new Error("Storage no responde (HeadBucket R2 o disco local)");
if (!config.isDev && !s.configured) { if (!config.isDev && !s.configured) {
throw new Error("S3_ENDPOINT/S3_BUCKET/S3_* son obligatorios fuera de desarrollo"); throw new Error("S3_ENDPOINT/S3_BUCKET/S3_* son obligatorios fuera de desarrollo");
} }
}), })),
]); ]);
Deno.serve({ port, hostname: "0.0.0.0" }, app.fetch); Deno.serve({ port, hostname: "0.0.0.0" }, app.fetch);

View file

@ -120,6 +120,20 @@ Ejemplo: `sudo docker logs migrate-pdyrt8ccp804tfmutefau0k6-052025710094`. `Unkn
El `docker stop … No such container` del helper de Coolify es ruido de limpieza, no la causa. El build de imágenes puede ser OK y el deploy igual falla en `migrate`. El `docker stop … No such container` del helper de Coolify es ruido de limpieza, no la causa. El build de imágenes puede ser OK y el deploy igual falla en `migrate`.
### `dependency api failed to start` / `api is unhealthy`
Liquibase ya corrió si ves `migrate … Exited` y acto seguido `api … Starting`. La API **no llega a escuchar** si Redis, S3 o los roles `_app` fallan (fail-fast antes de `Deno.serve`). Coolify entonces marca unhealthy en 1–2 s.
```bash
sudo docker logs api-<uuid>-<timestamp>
```
Busca `[startup] FAIL …`. Causas típicas:
- **Redis:** `REDIS_URL_IAM` / `REDIS_URL_CORE` con el UUID de **Postgres**. Redis tiene el suyo (Redis URL internal).
- **S3/R2:** faltan `S3_*` o HeadBucket falla (token Account API, Object Read & Write, bucket correcto, `S3_REGION=auto`).
- **Postgres `_app`:** Liquibase usa `*_OWNER`; el runtime usa `DATABASE_URL_PLATFORM`, `DATABASE_URL_IAM`, `DATABASE_URL_CORE` (passwords distintos).
### Coolify pide `PLATFORM_OWNER_PASSWORD`, `IAM_APP_PASSWORD`, `*_REDIS_PASSWORD`… ### Coolify pide `PLATFORM_OWNER_PASSWORD`, `IAM_APP_PASSWORD`, `*_REDIS_PASSWORD`…
Eso sale de **Reload Compose** mezclando el overlay de desarrollo. En producción **no las rellenes**. En el recurso: Compose file = `docker-compose.yml` únicamente. Borra esas 8 variables si Coolify las marcó Required. Siguen haciendo falta las URLs completas (`DATABASE_URL_*`, `REDIS_URL_*`). Eso sale de **Reload Compose** mezclando el overlay de desarrollo. En producción **no las rellenes**. En el recurso: Compose file = `docker-compose.yml` únicamente. Borra esas 8 variables si Coolify las marcó Required. Siguen haciendo falta las URLs completas (`DATABASE_URL_*`, `REDIS_URL_*`).