fix: Health-Endpunkte trennen Liveness von Readiness

Readiness- und Liveness-Probe zeigten beide auf "/" — eine
force-dynamic-Seite samt Datenbankabfragen und vollem React-Rendering, alle
10 bzw. 30 Sekunden.

Der teure Teil ist dabei nicht das eigentliche Problem. Entscheidend ist,
dass damit auch die LIVENESS-Probe an der Datenbank haengt: faellt die DB
voruebergehend aus, wertet Kubernetes den Container als tot und startet ihn
neu — woraufhin der neue Container laut Dockerfile-CMD zuerst
"prisma migrate deploy" und "prisma db seed" gegen genau die gestoerte
Datenbank faehrt. Aus einer voruebergehenden Stoerung wird so eine
Neustart-Schleife.

Jetzt zwei Endpunkte mit klar getrennter Aufgabe:

- /api/health       — nur "Prozess antwortet", ohne Datenbankzugriff.
                      Liveness-Probe.
- /api/health/ready — zusaetzlich "SELECT 1" gegen die Datenbank, sonst 503.
                      Readiness-Probe und Docker-Healthcheck.

Faellt die Readiness-Probe aus, nimmt Kubernetes den Pod nur aus dem
Load-Balancing und startet ihn nicht neu — genau das richtige Verhalten.
"SELECT 1" laeuft unveraendert auf PostgreSQL und MySQL/MariaDB und
beruehrt keine Tabelle, ist also unabhaengig vom Migrationsstand. Die
Fehlerdetails gehen nur ins Server-Log, nicht in die oeffentliche Antwort.

docker-compose.yml bekommt denselben Readiness-Endpunkt als Healthcheck,
mit start_period fuer den Migrations-/Seed-Lauf vor dem Serverstart.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5 committed 2026-09-15 10:01:06 +02:00
1 parent d3020cd4da
commit 4b76247648
4 files changed
+86 -2

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export const dynamic = "force-dynamic";
/**
* Readiness-Probe: darf dieser Pod Kundenverkehr bekommen? Anders als die
* Liveness-Probe (siehe ../route.ts) prueft sie die Datenbank, denn ohne DB
* kann die Seite weder Produkte anzeigen noch eine Bestellung annehmen.
*
* Faellt sie aus, nimmt Kubernetes den Pod nur aus dem Service-Load-Balancing
* heraus und startet ihn NICHT neu — genau das richtige Verhalten bei einer
* voruebergehenden DB-Stoerung.
*
* "SELECT 1" funktioniert unveraendert auf PostgreSQL und MySQL/MariaDB
* (beide Provider sind per DATABASE_PROVIDER moeglich, siehe lib/prisma.ts)
* und beruehrt keine Tabelle, ist also unabhaengig vom Migrationsstand.
*/
export async function GET(): Promise<NextResponse> {
try {
await prisma.$queryRaw`SELECT 1`;
return NextResponse.json(
{ status: "ready", database: "up" },
{ headers: { "Cache-Control": "no-store" } }
);
} catch (error: unknown) {
// Details nur ins Server-Log, nicht in die Antwort: der Endpunkt ist
// oeffentlich erreichbar und soll keine Verbindungszeichenfolge oder
// internen Hostnamen preisgeben.
console.error("Readiness-Probe: Datenbank nicht erreichbar", error);
return NextResponse.json(
{ status: "not-ready", database: "down" },
{ status: 503, headers: { "Cache-Control": "no-store" } }
);
}
}
+27
View File
@@ -0,0 +1,27 @@
import { NextResponse } from "next/server";
// Nie zwischengespeichert: eine aus dem Cache beantwortete Probe wuerde einen
// haengenden Prozess als gesund melden.
export const dynamic = "force-dynamic";
/**
* Liveness-Probe: beantwortet ausschliesslich die Frage "laeuft der
* Node-Prozess noch und nimmt er Verbindungen an?" — bewusst OHNE
* Datenbankzugriff.
*
* Grund fuer die Trennung von /api/health/ready: eine Liveness-Probe, die an
* der Datenbank haengt, macht aus einer voruebergehenden DB-Stoerung eine
* Neustart-Schleife. Kubernetes toetet den Container, der Container faehrt
* hoch und laesst dabei laut Dockerfile-CMD erst "prisma migrate deploy" und
* "prisma db seed" gegen genau die Datenbank laufen, die gerade nicht
* erreichbar ist — der Neustart verschlimmert die Lage, statt sie zu
* beheben. Vorher zeigten beide Probes auf "/", also auf eine
* force-dynamic-Seite samt DB-Abfragen und vollstaendigem React-Rendering
* alle 10 bzw. 30 Sekunden.
*/
export function GET(): NextResponse {
return NextResponse.json(
{ status: "ok", uptimeSeconds: Math.round(process.uptime()) },
{ headers: { "Cache-Control": "no-store" } }
);
}