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`.
4.2 KiB
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 copiesnode_modules/,.git/, etc. into the build context. → Mitigation:.dockerignoreexcludesnode_modules,.git,dist, andopenspec. -
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.