Fixes SSR asset staleness by switching all routes to runtime SSR (RenderMode.Server). Every request now hits the SSR server which fetches fresh API data before rendering HTML, eliminating the stale-asset flash entirely.
Root Cause
The production Dockerfile copied only browser/ to nginx, ignoring server/server.mjs. Prerender baked DEFAULT_CONFIG into static HTML with no API available at build time. Nginx served this stale HTML on every request.
Changes (9 files)
Dockerfile: serve stage uses node:22-alpine, installs nginx via apk, copies both browser/ and server/ bundles
docker-entrypoint.sh: starts Angular SSR Node process on port 4200, waits for readiness, exports SSR_API_BASE_URL, then exec nginx
nginx.conf.template: adds upstream ssr_backend block, proxies / to SSR backend, static assets served directly with 1y cache
app.server.routes.ts: all routes switched from RenderMode.Prerender to RenderMode.Server
server.ts: removed dead prerender code
main.server.ts: injects SSR-aware apiBaseUrl via APP_CONFIG
server.spec.ts: updated + added test suites (31 tests total)
vitest.config.ts: added server.spec.ts to include pattern
Request Flow
Browser -> nginx (port 80)
+ /api/* -> upstream backend (api:8000)
+ .js/.css/images -> static files from disk (1y cache)
+ /* -> SSR server (port 4200)
+ Angular SSR bootstraps app
+ StationConfigService.load() -> HttpClient -> localhost:80/api/station-config
-> nginx proxies to api:8000 -> real data
-> rendered HTML contains fresh assets
Trade-off
TTFB is slightly higher than serving prerendered static HTML, but AngularNodeAppEngine caches the bootstrap — only the first request per process pays the full cost. Subsequent requests reuse the bootstrapped app.
## Summary
Fixes SSR asset staleness by switching all routes to runtime SSR (RenderMode.Server). Every request now hits the SSR server which fetches fresh API data before rendering HTML, eliminating the stale-asset flash entirely.
## Root Cause
The production Dockerfile copied only `browser/` to nginx, ignoring `server/server.mjs`. Prerender baked `DEFAULT_CONFIG` into static HTML with no API available at build time. Nginx served this stale HTML on every request.
## Changes (9 files)
- **Dockerfile**: serve stage uses `node:22-alpine`, installs nginx via `apk`, copies both `browser/` and `server/` bundles
- **docker-entrypoint.sh**: starts Angular SSR Node process on port 4200, waits for readiness, exports `SSR_API_BASE_URL`, then `exec nginx`
- **nginx.conf.template**: adds `upstream ssr_backend` block, proxies `/` to SSR backend, static assets served directly with 1y cache
- **app.server.routes.ts**: all routes switched from `RenderMode.Prerender` to `RenderMode.Server`
- **server.ts**: removed dead prerender code
- **main.server.ts**: injects SSR-aware `apiBaseUrl` via `APP_CONFIG`
- **server.spec.ts**: updated + added test suites (31 tests total)
- **vitest.config.ts**: added `server.spec.ts` to include pattern
## Request Flow
```
Browser -> nginx (port 80)
+ /api/* -> upstream backend (api:8000)
+ .js/.css/images -> static files from disk (1y cache)
+ /* -> SSR server (port 4200)
+ Angular SSR bootstraps app
+ StationConfigService.load() -> HttpClient -> localhost:80/api/station-config
-> nginx proxies to api:8000 -> real data
-> rendered HTML contains fresh assets
```
## Trade-off
TTFB is slightly higher than serving prerendered static HTML, but `AngularNodeAppEngine` caches the bootstrap — only the first request per process pays the full cost. Subsequent requests reuse the bootstrapped app.
Root cause: Dockerfile copied only browser/ to nginx; server/server.mjs
was built but never deployed or executed. Prerender baked DEFAULT_CONFIG
into static HTML with no API at build time, causing stale content on
every initial paint.
Changes:
- Dockerfile: serve stage uses node:22-alpine, installs nginx via apk,
copies both browser/ and server/ bundles from build stage
- docker-entrypoint.sh: starts Angular SSR Node process on port 4200,
waits for readiness, exports SSR_API_BASE_URL, then exec nginx
- nginx.conf.template: adds upstream ssr_backend block (127.0.0.1:4200),
proxies / to SSR backend, static assets served directly with 1y cache
- app.server.routes.ts: all routes switched from RenderMode.Prerender
to RenderMode.Server for zero stale content
- server.ts: removed dead prerender code (PRERENDERED_ROUTES,
isPrerenderedRoute, allowStaticRouteRender)
- main.server.ts: injects SSR-aware apiBaseUrl via APP_CONFIG
(reads SSR_API_BASE_URL env var)
- server.spec.ts: updated 5 test suites + added 5 new suites (31 tests)
covering server.ts cleanup, RenderMode.Server, entrypoint lifecycle,
Dockerfile build, nginx upstream proxy, and API URL injection
- vitest.config.ts: added server.spec.ts to include pattern
Request flow: Browser -> nginx (port 80) -> SSR server (port 4200)
-> Angular SSR -> HttpClient -> localhost:80/api/station-config
-> nginx proxies to api:8000 -> fresh data rendered in HTML
- src/main.server.ts: process.env['SSR_API_BASE_URL'] instead of
process.env.SSR_API_BASE_URL to satisfy TS4111 index signature rule.
- server.spec.ts: update corresponding test assertions to match new syntax.
We have a deployment issue: /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found
We have a deployment issue: ```/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found
/docker-entrypoint.sh: line 12: envsubst: not found```
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200```
New startup failures in the frontend:
```[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200
[entrypoint] Starting nginx...
nginx: [emerg] "map" directive is not allowed here in /etc/nginx/conf.d/default.conf:2
[entrypoint] Starting Angular SSR server on port 4200...
[entrypoint] Waiting for SSR server to be ready...
Node SSR server listening on http://localhost:4200```
Alpine nginx includes conf.d/*.conf inside the http{} block, but our
template contained map/log_format directives and a server{} block that
conflicted with the include structure. Switched to a complete standalone
nginx.conf with proper user/events/http nesting.
fix: ensure SSR always fetches fresh assets from database
fix: use bracket notation for process.env access (TS5.9 strict)
fix: add gettext to Alpine image for envsubst in entrypoint
fix: use full nginx.conf instead of conf.d snippet to avoid directive nesting error
Build passes, 73/73 tests pass. Ready for staging deployment.
CI passed (run #133). All 4 commits verified:
- fix: ensure SSR always fetches fresh assets from database
- fix: use bracket notation for process.env access (TS5.9 strict)
- fix: add gettext to Alpine image for envsubst in entrypoint
- fix: use full nginx.conf instead of conf.d snippet to avoid directive nesting error
Build passes, 73/73 tests pass. Ready for staging deployment.
The Angular 21 builder generates angular-app-engine-manifest.mjs at build
time but does NOT inject the import into server.mjs. Without manually
importing and registering the manifest via ɵsetAngularAppEngineManifest,
AngularNodeAppEngine fails with 'Angular app engine manifest is not set'
and every request returns 404.
This matches the exact issue reported in PR #14 on staging where nginx
starts successfully but returns 'Not Found' for all paths.
The latest commit (1db84a3) fixes the nginx 404 issue reported above. Root cause: Angular 21 builder generates angular-app-engine-manifest.mjs but does NOT auto-inject the import into server.mjs. Without manual registration via setAngularAppEngineManifest(), AngularNodeAppEngine returns 404 on every route.
Build passes locally. 73/73 tests pass. Ready for merge — needs admin approval since the robot account review counts as PENDING (restricted account).
The latest commit (`1db84a3`) fixes the nginx 404 issue reported above. Root cause: Angular 21 builder generates `angular-app-engine-manifest.mjs` but does NOT auto-inject the import into `server.mjs`. Without manual registration via `setAngularAppEngineManifest()`, `AngularNodeAppEngine` returns 404 on every route.
**Fix applied in `server.ts`:**
```ts
import engineManifest from './angular-app-engine-manifest.mjs';
import { ɵsetAngularAppEngineManifest } from '@angular/ssr';
ɵsetAngularAppEngineManifest(engineManifest);
```
Build passes locally. 73/73 tests pass. Ready for merge — needs admin approval since the robot account review counts as PENDING (restricted account).
kfj001
scheduled this pull request to auto merge when all checks succeed 2026-08-07 16:42:46 -07:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Fixes SSR asset staleness by switching all routes to runtime SSR (RenderMode.Server). Every request now hits the SSR server which fetches fresh API data before rendering HTML, eliminating the stale-asset flash entirely.
Root Cause
The production Dockerfile copied only
browser/to nginx, ignoringserver/server.mjs. Prerender bakedDEFAULT_CONFIGinto static HTML with no API available at build time. Nginx served this stale HTML on every request.Changes (9 files)
node:22-alpine, installs nginx viaapk, copies bothbrowser/andserver/bundlesSSR_API_BASE_URL, thenexec nginxupstream ssr_backendblock, proxies/to SSR backend, static assets served directly with 1y cacheRenderMode.PrerendertoRenderMode.ServerapiBaseUrlviaAPP_CONFIGserver.spec.tsto include patternRequest Flow
Trade-off
TTFB is slightly higher than serving prerendered static HTML, but
AngularNodeAppEnginecaches the bootstrap — only the first request per process pays the full cost. Subsequent requests reuse the bootstrapped app.We have a deployment issue:
/docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not found /docker-entrypoint.sh: line 12: envsubst: not foundNew startup failures in the frontend:
Alpine nginx includes conf.d/*.conf inside the http{} block, but our template contained map/log_format directives and a server{} block that conflicted with the include structure. Switched to a complete standalone nginx.conf with proper user/events/http nesting.The nginx server starts but is now just returning
not foundto the client along with HTTP: 404 😵CI passed (run #133). All 4 commits verified:
Build passes, 73/73 tests pass. Ready for staging deployment.
The latest commit (
1db84a3) fixes the nginx 404 issue reported above. Root cause: Angular 21 builder generatesangular-app-engine-manifest.mjsbut does NOT auto-inject the import intoserver.mjs. Without manual registration viasetAngularAppEngineManifest(),AngularNodeAppEnginereturns 404 on every route.Fix applied in
server.ts:Build passes locally. 73/73 tests pass. Ready for merge — needs admin approval since the robot account review counts as PENDING (restricted account).