# Implementation Plan: Production-Grade UI Container Image

**Branch**: `011-ui-prod-dockerfile` | **Date**: 2026-05-07 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `specs/011-ui-prod-dockerfile/spec.md`

## Summary

Build a production-grade multi-stage Docker image for the Angular UI. A `node:22-slim` build stage compiles the Angular app into static assets; an `nginxinc/nginx-unprivileged:alpine` runtime stage serves those assets on port 8080 as a non-root user with SPA fallback routing, long-lived cache headers for fingerprinted bundles, and clean SIGTERM handling. The image is verified by a TDD shell script that covers all three user stories (reliable service, security, build caching) in one `make verify-ui-prod` run.

## Technical Context

**Language/Version**: Node.js 22 (build stage); no runtime language in the final image
**Primary Dependencies**: Angular CLI 19 (`npm run build`); nginx-unprivileged (runtime web server)
**Storage**: None — container serves pre-compiled static files
**Testing**: `ui/tests/build/verify_production_image.sh` (shell script TDD artefact, same pattern as `api/tests/build/verify_production_image.sh`)
**Target Platform**: Linux container (amd64); Docker 23+ with BuildKit enabled (default); `--mount=type=cache` used for npm cache layer
**Project Type**: Static file server (SPA)
**Performance Goals**: Cold build < 3 minutes; warm (source-only) rebuild < 30 seconds; health check response < 500ms
**Constraints**: Non-root process (UID ≠ 0); Node.js absent from runtime image; no secrets in image layers
**Scale/Scope**: Single container; no horizontal scaling concerns at this stage

## Constitution Check

### Pre-research gates

| Principle | Requirement | Status |
|-----------|-------------|--------|
| §5.1 TDD | Failing test (verify script) must exist before `Dockerfile.prod` | ✅ Plan includes TDD-first task ordering |
| §5.3 Tests next to code | `ui/tests/build/` mirrors `api/tests/build/` | ✅ Correct location |
| §5.4 CI before done | All tasks marked done only after verify passes | ✅ Enforced in task ordering |
| §7.1 One-command start | `docker compose up` must still work | ✅ Only adds prod Dockerfile; dev Dockerfile unchanged |
| §7.2 Env config | No hardcoded credentials in Dockerfile | ✅ No runtime env vars needed; build-time config via Angular environment files |
| §7.3 Linting | shellcheck on verify script | ✅ T011 in task plan |
| §8 Scope | Server-side rendering, OIDC, multi-user — not addressed | ✅ Spec scoped to static asset serving only |

**No violations. All gates pass.**

### Post-design re-check

Same gates apply. No design decisions introduced in Phase 1 conflict with the constitution.

## Project Structure

### Documentation (this feature)

```text
specs/011-ui-prod-dockerfile/
├── plan.md           ← this file
├── research.md       ← technology decisions (10 decisions)
├── contracts/
│   └── container.md  ← container interface contract
├── quickstart.md     ← build and verify scenarios
└── tasks.md          ← generated by /speckit-tasks
```

### Source Code Changes

```text
ui/
├── Dockerfile.prod             ← NEW (multi-stage production build)
├── nginx.conf                  ← NEW (SPA routing + cache headers)
├── .dockerignore               ← NEW (does not exist yet; created for production build)
└── tests/
    └── build/
        ├── .gitkeep            ← NEW (track directory in git)
        └── verify_production_image.sh  ← NEW (TDD verification script)

Makefile                        ← MODIFIED (add build-ui-prod, verify-ui-prod targets)
```

## Dockerfile Design

### Stage 1 — Builder (`node:22-slim`)

```
COPY package.json package-lock.json ./   # layer: deps (cached until lockfile changes)
RUN --mount=type=cache,target=/root/.npm npm ci  # reproducible install; npm cache mounted
COPY . .                                 # layer: source (invalidated on every change)
RUN npm run build                        # ng build --configuration production
```

Output of `npm run build`: `dist/reactbin-ui/browser/` (confirmed: Angular 19 application builder creates `browser/` subdirectory under `outputPath`).

### Stage 2 — Runtime (`nginxinc/nginx-unprivileged:alpine`)

- Runs as non-root by design (no manual `useradd` needed)
- Listens on port 8080
- `COPY --from=builder /app/dist/reactbin-ui/browser /usr/share/nginx/html`
- `COPY nginx.conf /etc/nginx/conf.d/default.conf`
- HEALTHCHECK via `wget` (curl not present in Alpine nginx-unprivileged)
- No CMD override needed — the base image entrypoint starts nginx

### nginx.conf

```nginx
server {
    listen 8080;

    root /usr/share/nginx/html;
    index index.html;

    # SPA fallback — unmatched paths return app shell
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Long-lived cache for fingerprinted assets
    location ~* \.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Never cache the entry point
    location = /index.html {
        add_header Cache-Control "no-store, no-cache, must-revalidate";
    }
}
```

## Verification Script Design (`ui/tests/build/verify_production_image.sh`)

Mirrors `api/tests/build/verify_production_image.sh` structure:

| Check | Story | Description |
|-------|-------|-------------|
| Build | US1 | `docker build -f ui/Dockerfile.prod ui/` succeeds |
| Health endpoint | US1 | `wget -q http://localhost:18080/` returns 200 within 30s |
| SPA routing | US1 | `curl http://localhost:18080/library` returns 200 |
| Graceful shutdown | US1 | `docker stop` → exit code 0 |
| Non-root user | US2 | `docker exec id -u` ≠ 0 |
| Node.js absent | US2 | `docker run node --version` exits non-zero |
| No secrets in history | US2 | `docker history --no-trunc` contains no secret-like strings |
| Dep layer cache hit | US3 | `touch ui/src/app/app.component.ts` + rebuild → output contains `CACHED` |

## Makefile Additions

```makefile
build-ui-prod:
    docker build -f ui/Dockerfile.prod ui/ -t reactbin-ui-prod:latest

verify-ui-prod:
    bash ui/tests/build/verify_production_image.sh
```

## Dependencies & Risks

| Item | Risk | Mitigation |
|------|------|------------|
| `dist/reactbin-ui/browser/` path | If Angular changes the output directory structure in a future version, the COPY path breaks | Path is verified in research; a test build during verify catches drift |
| `nginxinc/nginx-unprivileged` UID | UID may vary between image versions | Check is `UID ≠ 0`, not a specific UID value |
| `wget` availability | Alpine images may change toolset | HEALTHCHECK is tested as part of US1 verify |
| Port 18080 collision | Another process may use 18080 during verify | Acceptable risk for a dev-time test; port is not a system service |
