feat: UI/BFF docker + compose stack with Redis - #30
Conversation
Single multi-stage Dockerfile builds the Vite SPA and Fastify BFF into one runtime image (BFF serves the built UI as static files, matching vite.config.ts's outDir: "server/public" coupling). docker-compose.yml wires that image to a redis:7-alpine service. Image intentionally sets no NODE_ENV/COOKIE_SECURE/REDIS_URL, so config.ts's own fail-closed defaults apply untouched. Compose ships .env.docker.example with COOKIE_SECURE=false for a zero-config dev boot, but leaves REDIS_URL unset (stays on the in-process memory:// fallback) even though redis is running — opting into real Redis-backed sessions is a one-line uncomment. Adds DOCKER.md (quick start, env var reference, Redis opt-in, production checklist, joining an external network/stack) and a pointer link from README.md. Verified: alpine build (lightningcss musl prebuilts), clean compose boot, /healthz and / respond correctly, non-root user, Redis opt-in switches off the memory-redis warning, and COOKIE_SECURE=true without PUBLIC_ORIGIN/TRUST_PROXY still crash-loops as designed. Signed-off-by: Gabriel Costa <gabrielcg@proton.me>
- Flip docker-compose.yml's REDIS_URL default from memory:// to the
compose redis service (${REDIS_URL:-redis://redis:6379/0}); .env
overrides if set
- Replace .env.docker.example + server/.env.example with one root
.env.example, shared by native dev and Docker compose
- Add .env.prod.example (production-ready template, no unsafe defaults)
- Point server's dev/start scripts at ../.env so native dev reads the
same root .env compose uses, instead of a separate server/.env
- Rename FASTAPI_URL/FASTAPI_AUTH_HEADER_NAME env vars and their
config.ts properties to CONTEXTFORGE_URL/CONTEXTFORGE_AUTH_HEADER_NAME,
decoupling from the upstream's current framework choice
- Update DOCKER.md and README.md for all of the above
Verified: tsc --noEmit clean, all 39 server tests pass, native npm run
dev boots off root .env, Docker rebuild + compose up boots clean with
Redis as the default (no memory-redis warning), /healthz 200.
Signed-off-by: Gabriel Costa <gabrielcg@proton.me>
Signed-off-by: Gabriel Costa <gabrielcg@proton.me>
marekdano
left a comment
There was a problem hiding this comment.
Findings
1. docker-compose.yml silently downgrades a blank production REDIS_URL to the dev-only bundled Redis
File: docker-compose.yml:12
Severity: High
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} treats an empty (but present) REDIS_URL the same as unset, silently substituting the bundled dev-only redis service — bypassing config.ts's fail-closed check, which only rejects the literal memory://.
Failure scenario: A user runs cp .env.prod.example .env for a "production" compose deployment and fills in COOKIE_SECURE/PUBLIC_ORIGIN but leaves REDIS_URL= blank (as shipped, .env.prod.example:23). docker compose up then silently resolves REDIS_URL to redis://redis:6379/0 (verified via docker compose config: a blank REDIS_URL in .env → environment.REDIS_URL resolves to redis://redis:6379/0), so the app boots fine against the throwaway, non-persistent bundled redis container instead of erroring — directly contradicting .env.prod.example's comment "Real, persistent Redis — required" and DOCKER.md's claim that the app "won't boot at all" on a bad Redis config.
2. Healthcheck and port mapping hardcode 3000, ignoring the configurable PORT
File: Dockerfile:52 (also docker-compose.yml:7)
Severity: Medium
HEALTHCHECK hardcodes port 3000 instead of respecting the configurable PORT env var that server/src/index.ts actually listens on (fastify.listen({ port: config.port, ... })), and docker-compose.yml's port mapping ("3000:3000") has the same hardcoding.
Failure scenario: User sets PORT=8080 in .env (a documented, supported override in .env.example) and runs docker compose up. The app listens on 8080 inside the container, but HEALTHCHECK's fetch('http://127.0.0.1:3000/healthz') connects to a port nothing is listening on, so Docker marks the container permanently unhealthy (breaking the depends_on: service_healthy chain for anything depending on it), and the compose ports: 3000:3000 mapping no longer reaches the app from the host at all.
3. FASTAPI_URL → CONTEXTFORGE_URL rename is a silent breaking change
File: server/src/config.ts:572 (also server/package.json dev/start scripts)
Severity: Low–Medium
The rename is silent: optional() uses ??, so an old-named env var is simply ignored (not an error) and falls back to the default. server/package.json's dev/start scripts also switched from reading server/.env to ../.env (repo root), so a pre-existing server/.env isn't even read anymore.
Failure scenario: An existing contributor (or a CI/CD deployment with secrets still named FASTAPI_URL) pulls this branch without updating their env file/secret names. Previously this would point at a real gateway; now CONTEXTFORGE_URL silently falls back to config.ts's default http://127.0.0.1:4444 with no startup error, so /api/* calls in that container/deployment start failing with ECONNREFUSED instead of the deployment failing fast or picking up the intended upstream.
|
As per item 3, this is not released yet so there's no better time to change. If anyone is using this, they're doing so at their own risk. |
Signed-off-by: Gabriel Costa <gabrielcg@proton.me>
|
@marekdano Adrressed. |
Summary
Dockerizes the UI/BFF as a single image and adds a
docker-compose.ymlthat wires it up with Redis. Also unifies env config into one shared file and renames theFASTAPI_*env vars toCONTEXTFORGE_*to stop coupling to the upstream's current framework.What's new
Docker
Dockerfile— multi-stage build (UI deps/build, BFF deps/build, runtime). Builds the Vite SPA intoserver/public/and the Fastify BFF intoserver/dist/, then ships a minimal runtime image: non-root user,EXPOSE 3000,HEALTHCHECKon/healthz.docker-compose.yml—app+redis:7-alpine, with:REDIS_URLdefaulting to the composeredisservice (${REDIS_URL:-redis://redis:6379/0}), overridable via.envredis-datavolumeextra_hosts: host.docker.internal:host-gatewayso the app can reach a ContextForge instance running on the host (works out of the box on Docker Desktop; this makes it work on Linux too).dockerignoreDOCKER.md— quick start, env var reference, production checklist, joining an external network/stack, troubleshootingEnv config
server/.env.example+ a Docker-only example with one shared root.env.example.server/package.json'sdev/startnow read../.env, so native dev (cd server && npm run dev) anddocker compose upuse the exact same file..env.prod.example— a production-ready template where every var without a safe default is left blank/required (COOKIE_SECURE=true, realREDIS_URL,PUBLIC_ORIGIN, etc.), instead of shipping insecure defaults.NODE_ENV/COOKIE_SECURE/REDIS_URL—server/src/config.ts's own fail-closed defaults apply untouched; the dev-friendly posture comes entirely from.env.example.Rename:
FASTAPI_*→CONTEXTFORGE_*FASTAPI_URL→CONTEXTFORGE_URL,FASTAPI_AUTH_HEADER_NAME→CONTEXTFORGE_AUTH_HEADER_NAME, and the correspondingconfig.tsproperties, acrossconfig.ts,lib/upstream-auth.ts,lib/upstream-http-client.ts,routes/auth/{login,logout}.ts,routes/proxy/catch-all.ts, and 3 test files.README
DOCKER.md, updated the native Getting Started steps for the shared.env, updated the project structure diagram.Verification
tsc --noEmitand fullvitest run(39 tests) pass inserver/.docker buildsucceeds onnode:22-alpine(confirmedlightningcss's musl prebuilts work, no glibc fallback needed).docker compose up --buildboots clean with.env.exampleas-is:/healthz→200,/→ redirects to/app/loginand serves the SPA, non-root user, Redis (not memory://) backing sessions by default.CONTEXTFORGE_URL=http://host.docker.internal:<port>reaches a stub server on the host (fixes theECONNREFUSEDa container hits against0.0.0.0/127.0.0.1, which resolve to itself).config.tsstill fire inside the container (COOKIE_SECURE=truewithoutPUBLIC_ORIGIN/TRUST_PROXYcrash-loops as designed) — the image doesn't silently defang them.npm run devboots correctly reading the shared root.envvia the updated../.envpath.