Skip to content

feat: UI/BFF docker + compose stack with Redis - #30

Merged
gcgoncalves merged 4 commits into
mainfrom
dockerise
Aug 14, 2026
Merged

feat: UI/BFF docker + compose stack with Redis #30
gcgoncalves merged 4 commits into
mainfrom
dockerise

Conversation

@gcgoncalves

Copy link
Copy Markdown
Contributor

Summary

Dockerizes the UI/BFF as a single image and adds a docker-compose.yml that wires it up with Redis. Also unifies env config into one shared file and renames the FASTAPI_* env vars to CONTEXTFORGE_* 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 into server/public/ and the Fastify BFF into server/dist/, then ships a minimal runtime image: non-root user, EXPOSE 3000, HEALTHCHECK on /healthz.
  • docker-compose.ymlapp + redis:7-alpine, with:
    • REDIS_URL defaulting to the compose redis service (${REDIS_URL:-redis://redis:6379/0}), overridable via .env
    • a persistent redis-data volume
    • extra_hosts: host.docker.internal:host-gateway so 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)
  • .dockerignore
  • DOCKER.md — quick start, env var reference, production checklist, joining an external network/stack, troubleshooting

Env config

  • Replaced server/.env.example + a Docker-only example with one shared root .env.example. server/package.json's dev/start now read ../.env, so native dev (cd server && npm run dev) and docker compose up use the exact same file.
  • Added .env.prod.example — a production-ready template where every var without a safe default is left blank/required (COOKIE_SECURE=true, real REDIS_URL, PUBLIC_ORIGIN, etc.), instead of shipping insecure defaults.
  • The Dockerfile itself sets no NODE_ENV/COOKIE_SECURE/REDIS_URLserver/src/config.ts's own fail-closed defaults apply untouched; the dev-friendly posture comes entirely from .env.example.

Rename: FASTAPI_*CONTEXTFORGE_*

  • FASTAPI_URLCONTEXTFORGE_URL, FASTAPI_AUTH_HEADER_NAMECONTEXTFORGE_AUTH_HEADER_NAME, and the corresponding config.ts properties, across config.ts, lib/upstream-auth.ts, lib/upstream-http-client.ts, routes/auth/{login,logout}.ts, routes/proxy/catch-all.ts, and 3 test files.
  • Decouples config naming from the upstream gateway's current implementation (FastAPI), which ContextForge could change independently.

README

  • Points to DOCKER.md, updated the native Getting Started steps for the shared .env, updated the project structure diagram.

Verification

  • tsc --noEmit and full vitest run (39 tests) pass in server/.
  • docker build succeeds on node:22-alpine (confirmed lightningcss's musl prebuilts work, no glibc fallback needed).
  • docker compose up --build boots clean with .env.example as-is: /healthz200, / → redirects to /app/login and serves the SPA, non-root user, Redis (not memory://) backing sessions by default.
  • Confirmed CONTEXTFORGE_URL=http://host.docker.internal:<port> reaches a stub server on the host (fixes the ECONNREFUSED a container hits against 0.0.0.0/127.0.0.1, which resolve to itself).
  • Confirmed the fail-closed checks in config.ts still fire inside the container (COOKIE_SECURE=true without PUBLIC_ORIGIN/TRUST_PROXY crash-loops as designed) — the image doesn't silently defang them.
  • Native npm run dev boots correctly reading the shared root .env via the updated ../.env path.

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 marekdano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .envenvironment.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.

@gcgoncalves

Copy link
Copy Markdown
Contributor Author

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>
@gcgoncalves

Copy link
Copy Markdown
Contributor Author

@marekdano Adrressed.

@marekdano marekdano left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gcgoncalves - thanks for addressing the issues

The PR looks good now!

LGTM 🚀

@gcgoncalves
gcgoncalves merged commit efaa0c1 into main Aug 14, 2026
5 checks passed
@gcgoncalves gcgoncalves linked an issue Aug 14, 2026 that may be closed by this pull request
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.

[FEATURE]: Dockerise the react client

2 participants