feat: add Docker image, static server and GitLab CI

The server is a placeholder for the one in docs/ARCHITECTURE.md — it serves
public/ and a health endpoint, nothing more. Reads are confined to public/ by
resolving the path and checking it stays inside the root; string-matching
".." is not enough, since encodings and URL normalisation both change what
the string looks like and only the resolved path says which file would open.

The image runs typecheck and tests during build, ships no source or
toolchain, and runs unprivileged.

CI's feed job fails if the event count collapses. A source that quietly stops
yielding events is what a parser-only pipeline is most prone to, and nothing
else would surface it. Everything is offline, so a red pipeline always means
the code changed rather than a wiki being down.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-15 01:20:42 +02:00
co-authored by Claude Opus 5
parent 71bff72ee9
commit 517066dc65
7 changed files with 379 additions and 4 deletions
+42
View File
@@ -0,0 +1,42 @@
# Build the static site, then serve it from a distroless-ish runtime.
#
# Two stages so the image ships the built assets and nothing else: no source,
# no fixtures, no toolchain. The build is fully offline — it parses checked-in
# fixtures rather than fetching anything — so the image is reproducible and
# needs no network at build time.
FROM oven/bun:1.3-alpine AS build
WORKDIR /app
# Dependencies first, so a source-only change reuses this layer.
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY tsconfig.json index.html ./
COPY src ./src
COPY scripts ./scripts
COPY fixtures ./fixtures
COPY test ./test
# Fail the image on a type error or a failing test rather than shipping it.
RUN bun run typecheck && bun test
RUN bun run build
FROM oven/bun:1.3-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
# Run unprivileged. The bun image ships a `bun` user; use it rather than root.
COPY --from=build --chown=bun:bun /app/public ./public
COPY --from=build --chown=bun:bun /app/serve.ts ./serve.ts
USER bun
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD bun -e "await fetch('http://127.0.0.1:'+(process.env.PORT??3000)+'/api/health').then(r=>{if(!r.ok)process.exit(1)})"
CMD ["bun", "run", "serve.ts"]