From 4b76247648cb44cbccb5ceebfbf47eeab3284021 Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Tue, 15 Sep 2026 10:01:06 +0200 Subject: [PATCH] fix: Health-Endpunkte trennen Liveness von Readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docker-compose.yml | 10 +++++++++ k8s/05-app-deployment.yaml | 15 +++++++++++-- src/app/api/health/ready/route.ts | 36 +++++++++++++++++++++++++++++++ src/app/api/health/route.ts | 27 +++++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/app/api/health/ready/route.ts create mode 100644 src/app/api/health/route.ts diff --git a/docker-compose.yml b/docker-compose.yml index 318066d..7c86299 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,6 +79,16 @@ services: required: false expose: - "3000" + # Prueft denselben Endpunkt wie die Kubernetes-Readiness-Probe (siehe + # k8s/05-app-deployment.yaml): nicht nur "Prozess laeuft", sondern "App + # erreicht die Datenbank". start_period deckt den Migrations-/Seed-Lauf + # ab, den der Container laut Dockerfile-CMD vor dem Serverstart ausfuehrt. + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3000/api/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s volumes: # Persistiert hochgeladene Admin-Bilder/-Videos (Dockerfile legt # /app/uploads bereits mit passendem Besitzer fuer den non-root diff --git a/k8s/05-app-deployment.yaml b/k8s/05-app-deployment.yaml index f01a44d..bf64df0 100644 --- a/k8s/05-app-deployment.yaml +++ b/k8s/05-app-deployment.yaml @@ -60,15 +60,26 @@ spec: # (fehlende Volume-Referenz). - name: uploads mountPath: /app/uploads + # Prueft die Datenbank mit (siehe app/api/health/ready/route.ts). + # Schlaegt sie fehl, nimmt Kubernetes den Pod nur aus dem + # Load-Balancing, startet ihn aber nicht neu. readinessProbe: httpGet: - path: / + path: /api/health/ready port: 3000 initialDelaySeconds: 10 periodSeconds: 10 + # Bewusst der DB-FREIE Endpunkt (siehe app/api/health/route.ts): + # eine Liveness-Probe an der Datenbank wuerde aus einer + # voruebergehenden DB-Stoerung eine Neustart-Schleife machen, weil + # der neu startende Container laut Dockerfile-CMD zuerst + # "prisma migrate deploy" gegen dieselbe gestoerte Datenbank faehrt. + # Beide Probes zeigten vorher auf "/" — also auf eine + # force-dynamic-Seite mit DB-Abfragen und vollem React-Rendering + # alle 10 bzw. 30 Sekunden. livenessProbe: httpGet: - path: / + path: /api/health port: 3000 # initialDelaySeconds/failureThreshold grosszuegiger als sonst # ueblich: der Container migriert/seedet die DB selbst vor dem diff --git a/src/app/api/health/ready/route.ts b/src/app/api/health/ready/route.ts new file mode 100644 index 0000000..229a11e --- /dev/null +++ b/src/app/api/health/ready/route.ts @@ -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 { + 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" } } + ); + } +} diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..4c1ac24 --- /dev/null +++ b/src/app/api/health/route.ts @@ -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" } } + ); +}