## Context The project is a pure client-side React/Vite app (`app/` directory) with no backend or database. The build output is static HTML/JS/CSS in `app/dist/`. Currently there is no Dockerfile, docker-compose.yml, or any deployment configuration. The Vite config allows host `soos`, suggesting a specific deploy target. ## Goals / Non-Goals **Goals:** - Single-command production deployment via `docker compose up -d` - Small, efficient Docker image (static files served by nginx, no Node.js at runtime) - Reproducible builds — the Docker image builds from source with no host dependencies beyond Docker **Non-Goals:** - HTTPS/TLS termination (handled by a reverse proxy or load balancer in front) - CI/CD pipeline configuration - Multi-environment configs (dev/staging/prod) — one compose file for production - Health check endpoints (static file server doesn't need custom health checks) ## Decisions ### 1. Multi-stage Dockerfile: Node build → nginx serve **Choice**: Two-stage Dockerfile. Stage 1 (`node:22-alpine`) installs deps and runs `vite build`. Stage 2 (`nginx:alpine`) copies the built `dist/` into nginx's serve directory. **Rationale**: The final image contains only nginx + static files (~30 MB) instead of Node.js + node_modules (~400 MB). Alpine base keeps it minimal. **Alternative considered**: Single-stage Node image running `vite preview`. Rejected — `vite preview` is not production-grade and carries unnecessary runtime weight. ### 2. nginx for static file serving **Choice**: nginx:alpine as the production web server. **Rationale**: nginx is the standard for serving static SPAs — fast, lightweight, well-understood. It handles gzip compression, cache headers, and SPA routing fallback natively. **Alternative considered**: Caddy (automatic HTTPS, simpler config). Rejected — HTTPS termination is a non-goal and nginx is more widely deployed. ### 3. SPA fallback routing in nginx **Choice**: Configure nginx with `try_files $uri $uri/ /index.html` so all non-file routes fall back to index.html. **Rationale**: This is a single-page React app. If the user refreshes on any route, nginx needs to serve index.html and let React Router handle it. Even though this app currently has no client-side routing, this is the correct default for any SPA. ### 4. Gzip compression enabled in nginx **Choice**: Enable gzip for text/html, CSS, JS, and JSON in the nginx config. **Rationale**: The app's JS bundle benefits significantly from compression. nginx handles this with zero application changes. ### 5. Cache headers for hashed assets **Choice**: Set `Cache-Control: public, max-age=31536000, immutable` for files in `/assets/` (Vite's hashed output), and `no-cache` for `index.html`. **Rationale**: Vite produces content-hashed filenames for JS/CSS (e.g., `index-DaSE1W8K.css`), so they're safe to cache forever. `index.html` must always be revalidated to pick up new deploys. ### 6. Port 80 inside container, mapped via compose **Choice**: nginx listens on port 80 inside the container. `docker-compose.yml` maps a host port (default 8080) to container port 80. **Rationale**: Standard convention. The host port is easily changeable in compose without touching the Dockerfile or nginx config. ### 7. File layout — all Docker files at project root **Choice**: Place `Dockerfile`, `docker-compose.yml`, `nginx.conf`, and `.dockerignore` at the project root. **Rationale**: The Dockerfile needs access to `app/` for the build context. Placing it at root keeps the build context simple (`docker build .`). Compose convention is to put `docker-compose.yml` at root. ## Risks / Trade-offs - **No HTTPS**: The container serves plain HTTP on port 80. → Mitigation: Expected to sit behind a reverse proxy (Caddy, Traefik, cloud LB) that terminates TLS. - **Large build context if .dockerignore is missing**: Without `.dockerignore`, Docker copies `node_modules/`, `.git/`, etc. into the build context. → Mitigation: `.dockerignore` excludes `node_modules`, `.git`, `dist`, and `openspec`. - **nginx config is static**: No templating for environment-specific settings. → Mitigation: For this app there's nothing environment-specific. If needed later, envsubst can be added to the entrypoint.