Repository navigation
Conversation
- web/Dockerfile: multi-stage node build -> nginx static serve. - web/nginx.conf: SPA client-route fallback (try_files -> index.html), gzip, immutable caching for hashed /assets, no-cache index.html. - compose: add web service; split traefik routing so the API owns Host + PathPrefix(/api) (priority 10) and the web catch-all serves everything else (priority 1). The SPA's relative /api/v1 calls route to the api service — same contract as the Vite dev proxy. - .dockerignore (root + web/): keep API and web build contexts lean. Verified: docker build passes; container serves index (200), SPA fallback for client routes (/stock -> 200), assets carry immutable cache headers.
Owner
Author
|
Superseded — switching to single-container (SPA served by the API via wwwroot + MapFallbackToFile). Simpler deploy, zero CORS surface. New PR incoming. |
mforce
added a commit
that referenced
this pull request
Jul 15, 2026
* Serve the SPA from the API (single-container) Instead of a separate nginx container, the API now serves the built React/Vite SPA from wwwroot. One image, one deploy, one origin -> no CORS surface, relative /api/v1 calls are same-origin by construction. - Program.cs: UseDefaultFiles + UseStaticFiles (public, before auth) and MapFallbackToFile(index.html) at lowest route priority, so /api/v1/* and /health always match first and unknown paths fall back to the SPA for client-side routing. No-op in dev (Vite serves the SPA there). - src/Cluckwork.Api/Dockerfile: added a node:22-alpine web-build stage (npm ci + npm run build) whose dist/ is copied into wwwroot in the final image. - .dockerignore: keep the root build context lean (web/ source is now built in-image; node_modules/dist/bin/obj excluded). compose stays single-router (Host -> :8080). Supersedes the two-container approach (closed PR #27). Verified: docker build produces an image with the SPA in wwwroot; all 10 API integration tests pass with the new middleware in the pipeline. * Accept JWT PEM keys with escaped newlines from env .env files and env vars cannot hold real line breaks, so JWT keys are supplied with literal \n. RSA.ImportFromPem needs actual newlines — add PemKey.Normalize (\n -> newline, CRLF -> LF) and apply it at both import sites (public key in Program, private key in JwtTokenService). Unblocks docker compose, where Jwt__PublicKeyPem / Jwt__PrivateKeyPem come from deploy/.env. Verified: build clean, 10 integration tests pass (they sign + validate real tokens). * Make local docker dev work: publish API :8080, traefik prod-only - api: publish 8080:8080 for direct local access (http://localhost:8080 serves SPA + API), independent of traefik. - traefik: gate behind a 'prod' compose profile so a bare 'docker compose up' runs only api + db — no traefik. Docker Engine 29 rejects traefik's docker-provider API negotiation ('client version 1.24 too old'), which is irrelevant for local dev. Enable in prod with 'docker compose --profile prod up'. - bump traefik v3.1 -> v3.5. Verified: stack up, http://localhost:8080/ serves the SPA (200), /stock falls back to index (200), /health/live 200. Login is 500 until migrations + seed land (#5: relation "durable_jobs" does not exist). * Bind local API port to loopback only (127.0.0.1:8080) Security review: publishing 8080:8080 binds 0.0.0.0, exposing the plaintext (no-TLS) API to the LAN. Bind to 127.0.0.1 so local dev access stays on the host. Prod uses the traefik/TLS 'prod' profile. Verified: http://127.0.0.1:8080/ -> 200; docker port mapping shows 127.0.0.1:8080->8080/tcp. * Rename compose service api -> app (now bundles API + SPA) The service builds an image that serves both the JSON API and the SPA from wwwroot, so 'app' describes it better than 'api'. No code references the service name (DB uses Host=db); traefik discovers by container label, so labels are unchanged. Verified: docker compose config OK; deploy-app-1 serves / -> 200. --------- Co-authored-by: mforce <>
This was referenced Jul 21, 2026
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.
Containerizes the React/Vite SPA and adds it to the deploy stack. The backend Dockerfile already existed; this completes the picture.
Changes
web/Dockerfile— multi-stage:node:22-alpinebuild (npm ci && npm run build) →nginx:1.27-alpineserving staticdist/.web/nginx.conf— SPA client-route fallback (try_files → /index.html), gzip, immutable caching for hashed/assets/,no-cacheonindex.htmlso deploys are picked up.deploy/docker-compose.yml— newwebservice; traefik routing split so the API ownsHost && PathPrefix(/api)(priority 10) and the web catch-all serves everything else (priority 1). The SPA's relative/api/v1calls route to the api service — same contract as the Vite dev proxy, so no build-time API URL needed..dockerignore(root +web/) — keep both build contexts lean.Verification
docker build(web) passes./→ 200 index,/stockclient route → 200 (SPA fallback), hashed assets carryCache-Control: public, immutable.Supports #20 (SPA) and the deploy story. No app-code changes.