From fd9d32f5df7924f9eb754e6648b716c7af7431de Mon Sep 17 00:00:00 2001 From: Peter Gonda Date: Mon, 5 Oct 2026 20:05:25 +0200 Subject: [PATCH] panel: readiness-gate /api/health on Postgres (fix panel-roll 502s) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /api/health returned 200 immediately — the moment Next could answer an API route — so the panel's canary (update{health_check="checks"}) promoted before the app could serve pages/tRPC, and even with Postgres unreachable. Traefik cut traffic to an alloc that 502'd on real requests → the panel-roll downtime (measured ~11s of 502 across one roll). Now it reports ready only once Postgres answers (SELECT 1, 2s cap under the 3s check timeout), so the canary only promotes into a serving alloc — and the known PG-connect roll failure mode can't promote a DB-less alloc either. A ready latch keeps it green after first success so a steady-state DB blip can't flap the running panel out of Traefik; safe because the panel job has no check_restart. Co-Authored-By: Claude Opus 4.8 --- apps/dokploy/pages/api/health.ts | 40 +++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/pages/api/health.ts b/apps/dokploy/pages/api/health.ts index 57875a192..27f26d358 100644 --- a/apps/dokploy/pages/api/health.ts +++ b/apps/dokploy/pages/api/health.ts @@ -1,8 +1,46 @@ +import { db } from "@nomploy/server/db"; +import { sql } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; +// Readiness gate for the panel's Nomad health check. The panel job promotes its +// canary on this check (update { health_check = "checks" }). The old handler +// returned 200 immediately — the instant Next could answer an API route — so the +// canary promoted before the app could actually serve pages/tRPC (and even with +// Postgres unreachable). Traefik then cut traffic over to an alloc that 502'd on +// real requests: the panel-roll downtime. +// +// Now it reports ready only once Postgres answers, so the canary is only promoted +// into an alloc that can serve DB-backed requests (this also closes the known +// roll failure mode where a new alloc comes up before Postgres is accepting +// connections). The `ready` latch keeps it green after the first success, so a +// steady-state DB blip can't flap the already-serving panel out of Traefik — the +// gate applies to startup only, then behaves like the old always-200 liveness +// check. Safe because the panel job has no check_restart: a failing check only +// affects routing/promotion, it never kills the running alloc. +let ready = false; + +const DB_TIMEOUT_MS = 2000; // under the Nomad check timeout (3s) + export default async function handler( _req: NextApiRequest, res: NextApiResponse, ) { - return res.status(200).json({ ok: true }); + if (ready) { + return res.status(200).json({ ok: true }); + } + try { + await Promise.race([ + db.execute(sql`select 1`), + new Promise((_, reject) => + setTimeout( + () => reject(new Error("db readiness timeout")), + DB_TIMEOUT_MS, + ), + ), + ]); + ready = true; + return res.status(200).json({ ok: true, warmed: true }); + } catch { + return res.status(503).json({ ok: false, warming: true }); + } }