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:
90
specs/011-ui-prod-dockerfile/contracts/container.md
Normal file
90
specs/011-ui-prod-dockerfile/contracts/container.md
Normal 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.
|
||||
Reference in New Issue
Block a user