feat(dashboard): serve the bundled dashboard from NestJS on the API port#275
Merged
rmyndharis merged 1 commit intoJun 18, 2026
Merged
Conversation
Owner
|
Thanks @tobiasstrebitzer — this pair (#275 + #276) is a genuinely nice simplification (one service/port, less glue-config). I'm holding it out of v0.2.8 (just shipped) deliberately: it's a breaking deployment change (dashboard moves off Tracking it for v0.3.0. I'll review #275 then #276 as a unit and run a real serve-static smoke (SPA served + deep-link routing + |
rmyndharis
added a commit
that referenced
this pull request
Jun 17, 2026
Strict SemVer for 0.x: breaking changes bump the MINOR (0.3.0), non-breaking fixes/additions stay in 0.2.x; every breaking change carries a⚠️ + migration note. Refresh the release-summary table through v0.2.8, and re-frame v0.3.0 as the next breaking release (deployment simplification #275/#276 + Puppeteer config #265), with SDK/observability noted as incremental themes.
Contributor
Author
|
Thanks @rmyndharis, totally agree 👍 |
Serve the built dashboard from NestJS via @nestjs/serve-static so a single container/port (2785) serves both the API and the UI. ServeStaticModule is registered only when dashboard/dist/index.html exists (opt out with SERVE_DASHBOARD=false) and excludes /api and /socket.io so they keep returning real API/socket responses. main.ts logs a clear status line (served / disabled / build missing) so a missing build is obvious instead of a silent 404. The Dockerfile builds the dashboard in its builder stage (npm ci, reproducible) and copies dist into the runtime image. Remove the separate dashboard nginx container and its files; Traefik now routes everything to the API. Dev is unchanged: the Vite dev server on :2886 with HMR proxies /api and /socket.io to the API on :2785. Split-origin hosting still works via VITE_API_URL. Add build:all and prod scripts for non-Docker production runs, a serve-static regression test, a CHANGELOG entry with migration notes, and updated docs.
47ab2bb to
83a90ff
Compare
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
The dashboard and API currently run as two separate services. In production the dashboard is its
own nginx container (
dashboard/Dockerfile.traefik+dashboard/nginx.conf) that serves the SPAand reverse-proxies
/api+/socket.ioback to the API.This PR makes NestJS serve the prebuilt dashboard itself, on the same port as the API (default
2785), via the canonical@nestjs/serve-staticmodule. A single container/port now serves both the API and the UI, and the dashboard is available
by default wherever the API runs (no extra profile/container).
Motivation
break, and confuse adopters - extra config to keep in sync, an extra failure mode, more to document.
The standalone dashboard/nginx service has already caused real bugs: it proxied to the wrong backend
host (
openwainstead ofopenwa-api), broken for both/api/and/socket.io/until fix: Chromium singleton locks, crashpad --database, auth hang, and 502 Bad Gateway #259, andneeded a follow-up host-alignment fix in fix(docker): align dashboard nginx host + clear stale Chromium singleton locks #260 (
2732c5f). Folding the UI into the API deletes thatwhole class of glue-config bug.
plumbing that only existed to glue the two together - simpler to deploy and operate.
dashboard/src/services/api.tsdefaults to therelative
/api, the helmet CSP is all'self', and a CORS comment already references "the bundleddashboard served through the proxy."
docker-compose.dev.ymlran the dashboard as a second container (openwa+dashboardon:2886), while the productiondocker-compose.ymlgated it behind--profile with-dashboardso aplain
docker compose upstarted no dashboard at all. Now the UI is served by the API in both,with no separate container.
Net change
This PR is close to net-neutral on lines because it trades the deleted nginx container + configs for
the serve-static wiring, a regression test, docs, and a CHANGELOG entry - but it removes a whole
service:
package-lock.jsonchurn).services/containers (dashboard nginx + Traefik).
The win here isn't raw line count - it's one fewer moving part. PR2 is where the larger deletion lands.
What changed
Serving
src/app.module.ts: registersServeStaticModuleconditionally - only whendashboard/dist/index.htmlexists (mirrors the existing lazyqueueModulespattern). It serves theSPA with client-side fallback and excludes
/apiand/socket.ioso those keep returning realAPI/WebSocket responses (Express 5 / path-to-regexp v8 wildcard:
/api/{*splat}).SERVE_DASHBOARD=false.src/main.ts: logs a clear status line at startup - serving / disabled / build-missing - so amissing build is obvious instead of a silent 404. Also allows
fonts.googleapis.com/fonts.gstatic.comin the helmet CSP (the dashboard's webfonts are now governed by the API's CSP;without this they are blocked and the UI falls back to system fonts).
Build & packaging
Dockerfile: builder stage builds the dashboard (npm ciindashboard/, reproducible) and theruntime image copies
dashboard/dist.package.json: addsbuild:all(API + dashboard) andprod(build thennode dist/main) forrunning the production build directly without Docker, plus a
dashboard:cihelper.dashboard/Dockerfile,dashboard/Dockerfile.traefik,dashboard/nginx.conf) and thedashboardcompose services. Traefik now routes everything to the API.Dev experience is unchanged
npm run devstill runs the Vite dev server on:2886with HMR, proxying/api+/socket.iotothe API on
:2785.serve-staticstays inert in dev because no build is present.Docs / tests
CHANGELOG.mdentry with breaking + migration notes; README anddocs/*updated to the single-portmodel;
test/serve-static.e2e-spec.tsregression test.Breaking changes & migration
:2886(separate nginx container) to the API port:2785. Updatebookmarks, monitoring, and any external reverse-proxy config.
with-dashboardcompose profile and theDASHBOARD_ENABLEDenv var are removed (the dashboardships with the API; silently ignored if still set).
SERVE_DASHBOARD=falseopts out of serving the UI (API-only).Split-origin hosting is still supported (not a lock-in): build the dashboard with the API origin
baked in and host
dashboard/distanywhere (CDN, object store, separate container):VITE_API_URL=https://api.example.com npm run build # in dashboard/Then set
SERVE_DASHBOARD=falseon the API and add the dashboard origin toCORS_ORIGINS.Testing / verification
npm test(436),npm run test:e2e(9, incl. the new serve-static regression),npm run lint,docker compose config- all green./→index.html, client routes → SPA fallback, assets served,/api/ping→ controller,/api/<unknown>→ JSON 404 (not the SPA). Pins the Express 5 wildcard.0 console errors, 0 CSP violations, 0 failed requests. (The CSP font allowance above was added
because this check initially surfaced 3 CSP violations.)
Notes for reviewers
@Mcp()/Silkweave work is untouched; this is purely about serving the SPA.feat/remove-traefik) is stacked on this branch. Review/merge this first.