diff --git a/Dockerfile b/Dockerfile index db8340a..01712e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,10 @@ COPY . . RUN pnpm build FROM base AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=build /app/.output ./.output EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \ + CMD curl -sf http://localhost:3000/api/health || exit 1 CMD ["node", ".output/server/index.mjs"] diff --git a/server/api/health.get.ts b/server/api/health.get.ts new file mode 100644 index 0000000..a9c799d --- /dev/null +++ b/server/api/health.get.ts @@ -0,0 +1,89 @@ +import { z } from "zod"; + +const gallerySchema = z.array( + z.object({ + filename: z.string(), + url: z.string(), + width: z.number(), + height: z.number(), + exif: z.object({ + date: z.coerce.date(), + camera: z.string(), + lens: z.string(), + settings: z.object({ + aperture: z.string(), + shutter: z.string(), + iso: z.string(), + focalLength: z.string(), + }), + }), + }), +).nonempty(); + +const projectSchema = z.array( + z.object({ + order: z.number(), + scope: z.enum(["hobby", "work"]), + title: z.string(), + link: z.string().url(), + technologies: z.array(z.string()), + en: z.object({ + description: z.string(), + summary: z.string(), + tasks: z.array(z.string()), + }), + fr: z.object({ + description: z.string(), + summary: z.string(), + tasks: z.array(z.string()), + }), + }), +).nonempty(); + +export default defineEventHandler(async (event) => { + const checks: Record = {}; + const baseURL = `http://localhost:${process.env.PORT ?? 3000}`; + + try { + const raw = await $fetch("/api/gallery", { baseURL }); + const result = gallerySchema.safeParse(raw); + checks.gallery = { + ok: result.success, + error: result.success ? undefined : result.error.message, + }; + } catch (err) { + checks.gallery = { ok: false, error: String(err) }; + } + + try { + const html = await $fetch("/gallery", { baseURL }); + checks.galleryPage = { + ok: html.includes(""), + error: html.includes("") ? undefined : "response is not valid HTML", + }; + } catch (err) { + checks.galleryPage = { ok: false, error: String(err) }; + } + + try { + const items = await queryCollection(event, "projects").all(); + const result = projectSchema.safeParse(items); + checks.projects = { + ok: result.success, + error: result.success ? undefined : result.error.message, + }; + } catch (err) { + checks.projects = { ok: false, error: String(err) }; + } + + const allOk = Object.values(checks).every((c) => c.ok); + + if (!allOk) { + throw createError({ + statusCode: 503, + data: { status: "unhealthy", checks }, + }); + } + + return { status: "ok", checks }; +});