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>
101 lines
2.5 KiB
Markdown
101 lines
2.5 KiB
Markdown
# Quickstart: UI Production Image
|
||
|
||
## Prerequisites
|
||
|
||
- Docker with BuildKit enabled (default in Docker 23+)
|
||
- `make` available in the shell
|
||
|
||
## Build the Image
|
||
|
||
```bash
|
||
make build-ui-prod
|
||
# Equivalent: docker build -f ui/Dockerfile.prod ui/ -t reactbin-ui-prod:latest
|
||
```
|
||
|
||
Expected: Build completes in ~2 minutes on first run (npm install), ~15 seconds on subsequent source-only changes.
|
||
|
||
## Run the Container
|
||
|
||
```bash
|
||
docker run --rm -p 4200:8080 reactbin-ui-prod:latest
|
||
```
|
||
|
||
Open http://localhost:4200 — the app shell loads. Navigate to `/library` or `/tags` — the page loads (SPA routing returns `index.html`).
|
||
|
||
## Verify All Production Checks
|
||
|
||
```bash
|
||
make verify-ui-prod
|
||
```
|
||
|
||
This runs `ui/tests/build/verify_production_image.sh`, which exercises all three user stories:
|
||
|
||
```
|
||
[verify] Building reactbin-ui-prod:verify-<PID>...
|
||
[verify] Build OK
|
||
[verify] Polling health endpoint...
|
||
[verify] Health check passed
|
||
[verify] SPA routing OK (/library → 200)
|
||
[verify] Non-root user OK (UID <n>)
|
||
[verify] Stdout logging OK
|
||
[verify] Graceful shutdown OK (exit 0)
|
||
[verify] Node.js absent in runtime image OK
|
||
[verify] No secrets in image layers OK
|
||
[verify] Dep layer cache hit confirmed (US3 OK)
|
||
[verify] All checks passed (US1 + US2 + US3).
|
||
```
|
||
|
||
## Integration Test Scenarios
|
||
|
||
### Scenario 1: Initial Build (Cold Cache)
|
||
|
||
```bash
|
||
docker rmi reactbin-ui-prod:latest 2>/dev/null || true
|
||
make build-ui-prod
|
||
```
|
||
|
||
Expected: `npm ci` runs fully (~30–90s depending on network). All packages installed from lockfile.
|
||
|
||
### Scenario 2: Source-Only Rebuild (Warm Cache)
|
||
|
||
```bash
|
||
touch ui/src/app/app.component.ts
|
||
make build-ui-prod
|
||
```
|
||
|
||
Expected: `npm ci` step is CACHED (skipped). Only the Angular compilation runs (~10–20s).
|
||
|
||
### Scenario 3: Dependency Change (Cache Invalidation)
|
||
|
||
```bash
|
||
# Simulate a lockfile change
|
||
touch ui/package-lock.json
|
||
make build-ui-prod
|
||
```
|
||
|
||
Expected: `npm ci` runs fresh (cache miss is intentional and correct).
|
||
|
||
### Scenario 4: SPA Deep-Link Routing
|
||
|
||
```bash
|
||
docker run --rm -d -p 4200:8080 --name ui-test reactbin-ui-prod:latest
|
||
curl -sf http://localhost:4200/library # 200 + index.html
|
||
curl -sf http://localhost:4200/tags # 200 + index.html
|
||
curl -sf http://localhost:4200/nonexistent # 200 + index.html (Angular handles 404)
|
||
docker stop ui-test
|
||
```
|
||
|
||
### Scenario 5: Non-Root Assertion
|
||
|
||
```bash
|
||
docker run --rm reactbin-ui-prod:latest id
|
||
# Must NOT output uid=0(root)
|
||
```
|
||
|
||
### Scenario 6: No Node.js in Runtime Image
|
||
|
||
```bash
|
||
docker run --rm reactbin-ui-prod:latest node --version 2>&1
|
||
# Must exit non-zero (node not found)
|
||
```
|