feat: optional self-host (Docker) mode — additive, zero-dep#41
Merged
Conversation
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).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a standalone
server.js+Dockerfileso anyone can run ProfileKiton their own infrastructure without going through Vercel. The Vercel path
(
api/[endpoint].js) is unchanged and remains the default; the self-hostpath reuses the exact same handler files via a thin adapter.
What ships
ALLOWEDgate as the Vercel router, Express-style req/res adapter, staticpublic/serving with traversal guard,X-ProfileKit-Instanceheader on every response, graceful SIGTERM/SIGINT shutdown. Only auto-listens when run directly.node:22-slim, USER node, HEALTHCHECK via Node 22 fetch (no curl install), nonpm installstep (zero deps). Builds in seconds.docker compose up --build --scale web=3brings 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.dockerjob builds the image, waits for HEALTHCHECK, then independently curls/api/healthand/api/divider.Constraints honored
dependencies/devDependencies).ALLOWEDgate + path-traversal guard on the new path.run:step of the new CI job.Verified locally
Test plan
npm run check— clean (now covers root server.js too)npm test— 197/197 (was 186 + 11 new server tests)test (22)/test (24)greendockerjob green