Multi-stage Dockerfile builds the Vite app in Node 22 and serves static assets from nginx:alpine. Includes gzip compression, SPA fallback routing, immutable cache headers for hashed assets, and configurable port mapping (default 8080). Deploy with `docker compose up -d`.
73 lines
4.2 KiB
Markdown
73 lines
4.2 KiB
Markdown
## 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.
|