Skip to content

feat(config): runtime config + nginx /api proxy for k8s deploy#94

Merged
vredchenko merged 1 commit into
mainfrom
feat/runtime-config-and-api-proxy
May 21, 2026
Merged

feat(config): runtime config + nginx /api proxy for k8s deploy#94
vredchenko merged 1 commit into
mainfrom
feat/runtime-config-and-api-proxy

Conversation

@vredchenko
Copy link
Copy Markdown
Collaborator

Summary

Phase A of the smartem-frontend k8s deploy work (see prior PRs #74, #90, #91, #92, #93). Makes the SPA image deployable across environments without per-env rebuilds.

The released 0.1.0 image baked VITE_KEYCLOAK_* and VITE_API_ENDPOINT at build time, which meant the published bundle couldn't talk to a backend or Keycloak from k8s without rebuilding per environment. Two coupled changes solve both:

  1. Backend API moves to relative /api/ proxied by nginx. mutator.ts now always returns /api. The Dockerfile's nginx stage adds location /api/ that proxy_passes to ${BACKEND_HOST} via a variable + resolver, so the container starts even if the backend isn't ready yet and DNS resolution happens at request time. BACKEND_HOST defaults to smartem-http-api-service (the k8s service name). The vite dev server's existing /api proxy keeps working unchanged.

  2. Keycloak URL/realm/clientId and authEnabled move to runtime /config.json. main.tsx fetches /config.json before render, calls setRuntimeConfig(), then mounts AuthGate. auth/config.ts exposes getKeycloakConfig() and isAuthEnabled() backed by the loaded config (throws if read before load). The image ships a placeholder apps/smartem/public/config.json with dev defaults (Keycloak mock at localhost:30090, authEnabled: false); k8s ConfigMap mounts the per-environment file at /usr/share/nginx/html/config.json.

Other touches:

  • Dockerfile: copy nginx.conf as a template under /etc/nginx/templates/ so the nginx:alpine entrypoint runs envsubst on it. NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1 makes the image's 15-local-resolvers.envsh export the pod's resolver(s) into NGINX_LOCAL_RESOLVERS, and NGINX_ENVSUBST_FILTER scopes the substitution to just BACKEND_HOST and NGINX_LOCAL_RESOLVERS (so any $host/$remote_addr in the template stays as nginx vars, never accidentally substituted).
  • .env.example: drop the now-runtime VITE_KEYCLOAK_* / VITE_AUTH_ENABLED; document where to edit them for local dev (public/config.json).
  • Version bump to 0.2.0 in both apps/smartem/package.json and apps/smartem/src/version.ts. The release pipeline will tag smartem-frontend-v0.2.0 after merge.

Skipped on purpose:

  • No regeneration of packages/api/src/generated/**. The release workflow regenerates these at build time; the same OpenAPI spec yielded only orval-version-comment + timestamp churn. Out of scope.
  • Smartem-frontend#93 (version-check.ts wiring) is independent; not bundled here.

Local verification

  • npm ci, npm run typecheck, npm run check, npm run build:smartem all clean
  • docker build . succeeds end-to-end
  • Container starts with default (unreachable) BACKEND_HOST and serves /, /version, /config.json, SPA history fallback
  • docker run -v /tmp/x.json:/usr/share/nginx/html/config.json proves the ConfigMap-mount pattern overrides the placeholder
  • On a docker network with a stand-in backend, GET /api/ reverse-proxies through and returns the backend's response (verified with a local nginx:alpine mock)
  • Confirmed the substituted default.conf (inside the running container) has the correct resolver line and set $backend_upstream "smartem-http-api-service" directive

Follow-ups (Phase B, smartem-devtools)

Once this lands and 0.2.0 is on GHCR, the k8s manifests + ingress for the frontend can land in smartem-devtools (deployment+service per env, ConfigMap with per-env keycloak/auth values, kustomization updates). That's the next PR in this series.

Test plan

  • CI lint + typecheck + build pass
  • After merge, the release workflow produces ghcr.io/diamondlightsource/smartem-frontend:0.2.0rc{n} and :latest is unchanged (RC, not stable)
  • Tag smartem-frontend-v0.2.0 to produce the stable release once Phase B is ready to consume it

Make the SPA image deployable across environments without rebuilds.
The released 0.1.0 image baked VITE_KEYCLOAK_* and VITE_API_ENDPOINT
at build time, which meant the published bundle couldn't talk to a
backend or Keycloak from k8s without per-environment rebuilds.

Two changes, one motivation:

1. Backend API moves from a build-time-baked URL to a relative
   /api/ path proxied by the SPA pod's own nginx. mutator.ts now
   always returns /api; nginx.conf adds a location /api/ that
   proxy_passes to ${BACKEND_HOST} via a variable + resolver, so
   the container starts even if the backend isn't reachable yet
   and resolution happens at request time. BACKEND_HOST defaults
   to smartem-http-api-service (the k8s service name); k8s
   Deployments override via env, plain docker via -e.

2. Keycloak URL/realm/clientId and authEnabled move from build-
   time VITE_KEYCLOAK_* env vars to a runtime /config.json
   fetched at boot. main.tsx awaits the fetch, calls
   setRuntimeConfig(), then renders AuthGate; auth/config.ts
   exposes getKeycloakConfig() and isAuthEnabled() backed by the
   loaded config (throws if read before load). The image ships
   a placeholder config.json with dev-mock defaults; k8s
   ConfigMap mounts the per-environment file at
   /usr/share/nginx/html/config.json.

Other changes:

- Dockerfile: copy nginx.conf as a template under
  /etc/nginx/templates/ so the nginx:alpine entrypoint runs
  envsubst on it. NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1 makes the
  image's 15-local-resolvers.envsh export the pod's resolver(s)
  into NGINX_LOCAL_RESOLVERS, and NGINX_ENVSUBST_FILTER scopes
  envsubst to BACKEND_HOST and NGINX_LOCAL_RESOLVERS only.
- .env.example: drop the now-runtime VITE_KEYCLOAK_*/
  VITE_AUTH_ENABLED; document where to edit them for local dev.
- Version bump to 0.2.0 in both apps/smartem/package.json and
  apps/smartem/src/version.ts.

The vite dev server's existing /api proxy keeps working for
npm run dev:smartem; /config.json is served from public/.

Verified locally:
- docker build succeeds end-to-end
- container starts with default unreachable BACKEND_HOST
- GET / serves the SPA, /version returns the build JSON,
  /config.json returns the placeholder
- mounting an alternate /config.json at runtime overrides the
  placeholder (k8s ConfigMap pattern)
- /api/ proxies through nginx to a stand-in backend on a
  shared docker network
- SPA history-mode fallback still works for client routes
@vredchenko vredchenko merged commit a37b75b into main May 21, 2026
9 checks passed
@vredchenko vredchenko deleted the feat/runtime-config-and-api-proxy branch May 21, 2026 10:48
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.

1 participant