Feat: Add production-grade multi-stage container image for UI

Two-stage build (node:22-slim builder + nginxinc/nginx-unprivileged:alpine
runtime) with SPA fallback routing, long-lived cache headers for fingerprinted
assets, non-root user (UID 101), and no Node.js toolchain in runtime image
(82 MB vs 329 MB+ single-stage). Verified by ui/tests/build/verify_production_image.sh
covering build, health, SPA routing, non-root, stdout logging, cache-control
headers, SIGTERM exit 0, Node.js absent, secret-free layers, and dep-layer
cache hit. 102 integration tests still pass; shellcheck clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 20:18:55 +00:00
parent 12176471e1
commit 1b3468b72d
16 changed files with 885 additions and 3 deletions

View File

@@ -0,0 +1,90 @@
# Container Interface Contract: UI Production Image
## Image Identity
| Property | Value |
|-------------|------------------------------|
| Image name | `reactbin-ui-prod` |
| Runtime | nginx-unprivileged (Alpine) |
| Listen port | `8080` |
| Run user | non-root (UID ≠ 0) |
## Runtime Inputs
### Environment Variables
The UI container is a static file server. It has **no required environment variables at runtime** — all configuration is compiled into the static assets at build time by the Angular build toolchain.
> Note: The API base URL is baked in at build time via Angular's environment configuration. A future iteration may introduce runtime environment injection via a served `config.json`, but this is out of scope for v1.
## Runtime Outputs
### HTTP Interface
| Route pattern | Behaviour |
|--------------------|-------------------------------------------------------------------|
| `/` | Returns `index.html` with HTTP 200 |
| `/` (any SPA path) | Returns `index.html` with HTTP 200 (SPA fallback via `try_files`)|
| `/main.*.js` | Returns fingerprinted JS bundle with long-lived cache headers |
| `/styles.*.css` | Returns fingerprinted CSS with long-lived cache headers |
| `/assets/*` | Returns static assets |
| Any path not found | Returns `index.html` with HTTP 200 (Angular router handles 404) |
### Cache Headers
| Asset type | Cache-Control header |
|-------------------------------------|-----------------------------------------------|
| Fingerprinted bundles (`.js`, `.css`, fonts) | `public, max-age=31536000, immutable` |
| `index.html` | `no-store, no-cache, must-revalidate` |
### Process Exit
| Signal | Expected exit code | Maximum wait |
|----------|--------------------|--------------|
| SIGTERM | 0 | 30 seconds |
| SIGKILL | non-zero | immediate |
## Health Check
| Property | Value |
|-----------------|--------------------------------|
| Command | `wget -qO- http://localhost:8080/` |
| Interval | 30 seconds |
| Timeout | 5 seconds |
| Start period | 15 seconds |
| Retries | 3 |
The health check passes when nginx responds with any 2xx status on the root path.
## Image Constraints
| Constraint | Requirement |
|-------------------------|-----------------------------------------------|
| Node.js runtime present | MUST NOT be present in runtime image |
| `node_modules/` present | MUST NOT be present in runtime image |
| Source TypeScript files | MUST NOT be present in runtime image |
| Secrets in layer history| MUST NOT appear in any `docker history` layer |
| Run as root | MUST NOT — process UID MUST be non-zero |
## Build Interface
| Property | Value |
|-----------------|----------------------------------------------|
| Dockerfile path | `ui/Dockerfile.prod` |
| Build context | `ui/` directory |
| Build command | `docker build -f ui/Dockerfile.prod ui/ -t reactbin-ui-prod:latest` |
### Build Context Exclusions (`.dockerignore`)
The following MUST be excluded from the build context to keep transfers fast and avoid leaking dev state:
- `node_modules/` — always rebuilt via `npm ci` in the build stage
- `dist/` — always rebuilt; must not pollute the build stage
- `.git/` — not needed for build
- `*.spec.ts` — test files not compiled into production output
- `.env*` — dev environment files
- `src/**/*.spec.ts` — test specs
## Verification
The contract is verified end-to-end by `ui/tests/build/verify_production_image.sh`. Running `make verify-ui-prod` MUST pass all contract checks.