# Azaion UI — Containerization

> Synthesis output of `/document` Step 3d (containerization). Derived from
> `Dockerfile`, `nginx.conf`, and `00_discovery.md` §3.

## 1. Image

**Multi-stage build** (`Dockerfile`):

| Stage | Base image | Role |
|-------|------------|------|
| 1 (builder) | `oven/bun:1.3.11-alpine` | `bun install --frozen-lockfile` + `bun run build` (= `tsc -b && vite build`) → `dist/` |
| 2 (runtime) | `nginx:alpine` | Serves `/usr/share/nginx/html` (`dist/`); listens on `:80` |

**Why this shape**:
- Bun gives a fast install + build vs. npm/yarn/pnpm.
- nginx alpine is a sub-25 MB runtime that already has reverse-proxy routing for `/api`.
- No Node runtime in production → smaller attack surface, faster startup, lower memory.

**Image labels** (OCI, set by Woodpecker CI):
- `org.opencontainers.image.revision = $CI_COMMIT_SHA`
- `org.opencontainers.image.created = $CI_BUILD_CREATED`
- `org.opencontainers.image.source = <repo url>`

**Environment**:
- `AZAION_REVISION = $CI_COMMIT_SHA` — accessible at runtime for diagnostics.
- No other env vars consumed at runtime by the SPA bundle (the bundle is fully static).

## 2. nginx routing (`nginx.conf`)

The image's nginx config strips `/api/<service>/` and reverse-proxies to the matching suite service inside the container network.

| Public path | Upstream (intra-cluster) | Service |
|-------------|--------------------------|---------|
| `/api/annotations/` | `http://annotations:8080/` | `annotations/` |
| `/api/flights/` | `http://flights:8080/` | `flights/` |
| `/api/admin/` | `http://admin:8080/` | `admin/` |
| `/api/resource/` | `http://resource:8080/` | `resource/` |
| `/api/detect/` | `http://detect:8080/` | `detect/` |
| `/api/loader/` | `http://loader:8080/` | `loader/` |
| `/api/gps-denied-desktop/` | `http://gps-denied-desktop:8080/` | `gps-denied-desktop/` |
| `/api/gps-denied-onboard/` | `http://gps-denied-onboard:8080/` | `gps-denied-onboard/` |
| `/api/autopilot/` | `http://autopilot:8080/` | `autopilot/` |
| `/` (any other path) | static fallback to `/index.html` (SPA routing) | — |

**Body size cap**: `client_max_body_size 500M` — tlog + video uploads in GPS-Denied Test Mode and large image uploads in Annotations both ride this limit.

**Headers passed to upstream**: standard `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto` (assumed — verify in `nginx.conf`).

**SSE handling**: `proxy_buffering off` MUST be set on `/api/detect/` and any other path that streams (Step 4 verification — confirm in `nginx.conf`).

## 3. Resource sizing (recommended, not enforced)

| Resource | Recommendation | Rationale |
|----------|----------------|-----------|
| CPU | 100 m (0.1 vCPU) | nginx is near-idle; 99 % of work is suite services |
| Memory | 32 Mi | nginx + ~5 MB of static assets |
| Storage | ephemeral 50 Mi | bundle is sub-5 MB gzipped today; some headroom |
| Replicas | 1+ | trivially horizontal; HA only matters if the ingress sits in front |

**Bundle size budget**: `vite build` output should stay under ~2 MB gzipped initial JS. Currently `chart.js` and `leaflet` are the dominant chunks; `AltitudeChart` is a lazy-load candidate (finding in `05_flights`).

## 4. Health checks

**Today: none.**

Recommended (Step 4 / Step 6 surface):
- **Liveness**: `GET /index.html → 200`
- **Readiness**: same (the SPA has no warm-up)
- **Container health**: `wget --spider -q http://localhost/index.html`

The suite-level orchestrator (parent suite docker-compose / k8s) is expected to handle ingress health-checking; individual UI replicas don't need their own.
