Skip to content

feat: optional self-host (Docker) mode — additive, zero-dep#41

Merged
heznpc merged 1 commit into
mainfrom
chore/self-host-2026-05-29
Jun 3, 2026
Merged

feat: optional self-host (Docker) mode — additive, zero-dep#41
heznpc merged 1 commit into
mainfrom
chore/self-host-2026-05-29

Conversation

@heznpc

@heznpc heznpc commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds a standalone server.js + Dockerfile so anyone can run ProfileKit
on their own infrastructure without going through Vercel. The Vercel path
(api/[endpoint].js) is unchanged and remains the default; the self-host
path reuses the exact same handler files via a thin adapter.

What ships

  • server.js — Node 22 http server with the same ALLOWED gate as the Vercel router, Express-style req/res adapter, static public/ serving with traversal guard, X-ProfileKit-Instance header on every response, graceful SIGTERM/SIGINT shutdown. Only auto-listens when run directly.
  • Dockerfilenode:22-slim, USER node, HEALTHCHECK via Node 22 fetch (no curl install), no npm install step (zero deps). Builds in seconds.
  • .dockerignore — trims tests/docs/examples/.git from the build context.
  • examples/self-host/docker compose up --build --scale web=3 brings up 3 app replicas behind nginx round-robin. 128 MB / 0.5 CPU per replica mirrors the Vercel function budget. README documents the per-process token-pool limitation.
  • tests/server.test.js — 11 new smoke tests (197/197 total).
  • .github/workflows/ci.yml — new docker job builds the image, waits for HEALTHCHECK, then independently curls /api/health and /api/divider.
  • README — 5-label block updated; Self-hosting split into Path A (Vercel default) + Path B (Docker).

Constraints honored

  • Zero runtime npm dependencies preserved (package.json still has no dependencies / devDependencies).
  • Same SSRF / XSS guards via shared handlers; ALLOWED gate + path-traversal guard on the new path.
  • Host header untrusted (URL parsed against a fixed origin).
  • No untrusted GitHub event data in any run: step of the new CI job.

Verified locally

docker build -t profilekit:local .   # succeeds
docker run -d -p 13000:3000 profilekit:local
curl -s http://localhost:13000/api/health        # ok:true
curl -sI http://localhost:13000/api/health       # X-ProfileKit-Instance: <container-id>
curl -s -o /dev/null -w '%{content_type}\n' \
  'http://localhost:13000/api/divider?style=line'  # image/svg+xml

Test plan

  • npm run check — clean (now covers root server.js too)
  • npm test — 197/197 (was 186 + 11 new server tests)
  • Local docker build + run smoke
  • CI matrix test (22) / test (24) green
  • CI docker job green

Adds a standalone server.js + Dockerfile so anyone can run ProfileKit
on their own infrastructure without going through Vercel. The Vercel
path (api/[endpoint].js) is unchanged and remains the default; the
self-host path reuses the exact same handler files via a thin adapter.

What ships
- server.js (repo root): Node 22 http server. Reproduces the Vercel
  glue (Express-style req.query / res.status() / res.send(), route
  /api/<endpoint> to src/endpoints/<endpoint>) with the same ALLOWED
  gate as api/[endpoint].js so dynamic require cannot escape the
  endpoints directory. Serves public/ with a path-traversal guard.
  Stamps X-ProfileKit-Instance on every response (replica visibility
  behind a load balancer). Graceful SIGTERM/SIGINT shutdown. Only
  auto-listens when run directly so tests can drive .listen(0).
- Dockerfile: node:22-slim, USER node, /api/health HEALTHCHECK via
  Node 22's built-in fetch (no curl install), no `npm install` step
  (zero runtime deps). Builds in seconds.
- .dockerignore: keeps tests / docs / examples / .git out of the
  build context.
- examples/self-host/{docker-compose.yml, nginx/nginx.conf, README.md}:
  three app replicas behind one nginx LB doing per-request DNS-based
  round-robin. Resource limits (128 MB / 0.5 CPU) mirror the Vercel
  function budget so behavior under load matches production. Example
  README documents the known limitation that GitHub token pool state
  is per-process — for high-volume self-hosts, give each replica its
  own token via GITHUB_TOKENS= or GITHUB_TOKEN_1..N, or front the
  deployment with a shared rate-limit store (Redis).

Tests
- tests/server.test.js: boots server.listen(0), exercises /api/health
  + /api/divider + /api/catalog + /api/<unknown> (404) + static
  index.html + /robots.txt + traversal attempts. 11 new tests,
  197/197 total.

CI
- .github/workflows/ci.yml gains a `docker` job that builds the
  image, runs the container, waits for HEALTHCHECK to flip to
  "healthy", then independently curls /api/health (asserts ok:true)
  and /api/divider (asserts image/svg+xml). No untrusted GitHub
  event data flows into any run: step.

README
- 5-label refresh: dual deployment paths in Currently implemented;
  Design intent clarifies the handlers are shared; Non-goals state
  explicitly that Docker mode does NOT replace the Vercel path.
- Self-hosting section split into Path A (Vercel) + Path B (Docker)
  with the per-process token-pool limitation called out.

Constraints honored
- Zero runtime npm dependencies (package.json has no dependencies /
  devDependencies keys; the new server is pure node:http + node:fs).
- Security: same SSRF / XSS guards via shared handlers; ALLOWED gate
  + path traversal guard for the self-host path; host header is not
  trusted (URL parsed against a fixed origin).
@heznpc heznpc enabled auto-merge (squash) June 3, 2026 17:31
@heznpc heznpc merged commit 1548f6d into main Jun 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant