Add deployment infrastructure: health endpoint, Docker Compose fixes, tunnel
- Add /api/health endpoint checking DB, pgvector, org, templates, dev bypass safety, and AUTH_SECRET presence - Fix Docker Compose app service: AUTH_SECRET, Entra ID env vars, AUTH_TRUST_HOST, app health check - Add Cloudflare Tunnel service for zero-config HTTPS access - Exclude health endpoint from auth middleware Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0eaf809bc6
commit
fa55dfc25f
3 changed files with 116 additions and 4 deletions
|
|
@ -57,8 +57,12 @@ services:
|
|||
OLLAMA_EMBED_MODEL: nomic-embed-text
|
||||
OLLAMA_LLM_MODEL: qwen3:1.7b
|
||||
NODE_ENV: production
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-dev-secret-change-in-production}
|
||||
AUTH_SECRET: ${AUTH_SECRET}
|
||||
AUTH_TRUST_HOST: "true"
|
||||
AUTH_MICROSOFT_ENTRA_ID_ID: ${AUTH_MICROSOFT_ENTRA_ID_ID}
|
||||
AUTH_MICROSOFT_ENTRA_ID_SECRET: ${AUTH_MICROSOFT_ENTRA_ID_SECRET}
|
||||
AUTH_MICROSOFT_ENTRA_ID_TENANT_ID: ${AUTH_MICROSOFT_ENTRA_ID_TENANT_ID}
|
||||
CRON_SECRET: ${CRON_SECRET:-change-me}
|
||||
volumes:
|
||||
- uploads_data:/data/uploads
|
||||
depends_on:
|
||||
|
|
@ -66,6 +70,25 @@ services:
|
|||
condition: service_healthy
|
||||
ollama:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
profiles:
|
||||
- production
|
||||
|
||||
# ─── Cloudflare Tunnel (HTTPS access without port forwarding) ──
|
||||
tunnel:
|
||||
image: cloudflare/cloudflared:latest
|
||||
restart: unless-stopped
|
||||
command: tunnel run
|
||||
environment:
|
||||
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN}
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- production
|
||||
|
||||
|
|
|
|||
88
src/app/api/health/route.ts
Normal file
88
src/app/api/health/route.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
const checks: Record<string, { status: "ok" | "fail"; detail?: string }> = {};
|
||||
let healthy = true;
|
||||
|
||||
// 1. Database connectivity
|
||||
try {
|
||||
await prisma.$queryRawUnsafe("SELECT 1");
|
||||
checks.database = { status: "ok" };
|
||||
} catch (e: any) {
|
||||
checks.database = { status: "fail", detail: e.message };
|
||||
healthy = false;
|
||||
}
|
||||
|
||||
// 2. pgvector extension
|
||||
try {
|
||||
const result: any[] = await prisma.$queryRawUnsafe(
|
||||
"SELECT extversion FROM pg_extension WHERE extname = 'vector'"
|
||||
);
|
||||
if (result.length > 0) {
|
||||
checks.pgvector = { status: "ok", detail: `v${result[0].extversion}` };
|
||||
} else {
|
||||
checks.pgvector = { status: "fail", detail: "extension not installed" };
|
||||
healthy = false;
|
||||
}
|
||||
} catch {
|
||||
checks.pgvector = { status: "fail", detail: "query failed" };
|
||||
healthy = false;
|
||||
}
|
||||
|
||||
// 3. Organization exists
|
||||
try {
|
||||
const orgCount = await prisma.organization.count();
|
||||
if (orgCount > 0) {
|
||||
checks.organization = { status: "ok", detail: `${orgCount} org(s)` };
|
||||
} else {
|
||||
checks.organization = { status: "fail", detail: "no organizations found" };
|
||||
healthy = false;
|
||||
}
|
||||
} catch {
|
||||
checks.organization = { status: "fail", detail: "query failed" };
|
||||
healthy = false;
|
||||
}
|
||||
|
||||
// 4. Pipeline templates exist
|
||||
try {
|
||||
const templateCount = await prisma.pipelineTemplate.count();
|
||||
checks.pipeline_templates = {
|
||||
status: templateCount > 0 ? "ok" : "fail",
|
||||
detail: `${templateCount} template(s)`,
|
||||
};
|
||||
if (templateCount === 0) healthy = false;
|
||||
} catch {
|
||||
checks.pipeline_templates = { status: "fail", detail: "query failed" };
|
||||
healthy = false;
|
||||
}
|
||||
|
||||
// 5. DEV_BYPASS_AUTH safety
|
||||
const devBypassActive =
|
||||
process.env.DEV_BYPASS_AUTH === "true" && process.env.NODE_ENV === "production";
|
||||
if (devBypassActive) {
|
||||
checks.dev_bypass = { status: "fail", detail: "DEV_BYPASS_AUTH=true in production!" };
|
||||
healthy = false;
|
||||
} else {
|
||||
checks.dev_bypass = { status: "ok" };
|
||||
}
|
||||
|
||||
// 6. AUTH_SECRET set (required for next-auth v5)
|
||||
if (process.env.NODE_ENV === "production" && !process.env.AUTH_SECRET) {
|
||||
checks.auth_secret = { status: "fail", detail: "AUTH_SECRET not set" };
|
||||
healthy = false;
|
||||
} else {
|
||||
checks.auth_secret = { status: "ok" };
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: healthy ? "healthy" : "unhealthy",
|
||||
checks,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
{ status: healthy ? 200 : 503 }
|
||||
);
|
||||
}
|
||||
|
|
@ -11,9 +11,10 @@ export function middleware(request: NextRequest) {
|
|||
const isAuthPage = pathname.startsWith("/login");
|
||||
const isPendingPage = pathname.startsWith("/pending");
|
||||
const isApiAuth = pathname.startsWith("/api/auth");
|
||||
const isHealthCheck = pathname === "/api/health";
|
||||
|
||||
// Always allow auth API routes
|
||||
if (isApiAuth) {
|
||||
// Always allow auth API routes and health checks
|
||||
if (isApiAuth || isHealthCheck) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue