## ADDED Requirements ### Requirement: Multi-stage Docker build The Dockerfile SHALL use a multi-stage build where stage 1 installs dependencies and builds the Vite app, and stage 2 copies only the built static assets into an nginx:alpine image. #### Scenario: Build produces a working image - **WHEN** `docker compose build` is run from the project root - **THEN** a Docker image is produced that contains only nginx and the built static files from `app/dist/` #### Scenario: Node.js is not present in the final image - **WHEN** the final Docker image is inspected - **THEN** it MUST NOT contain Node.js, npm, or `node_modules` ### Requirement: Single-command deployment with docker compose The project SHALL include a `docker-compose.yml` that builds and runs the app with `docker compose up`. #### Scenario: Start the app - **WHEN** `docker compose up -d` is run from the project root - **THEN** the app is built (if needed) and served on the configured host port #### Scenario: Rebuild after code changes - **WHEN** `docker compose up -d --build` is run after source code changes - **THEN** the image is rebuilt with the latest code and the container is replaced ### Requirement: nginx serves the SPA correctly nginx SHALL serve the app's static files and fall back to `index.html` for all non-file routes. #### Scenario: Root path serves the app - **WHEN** a browser requests `/` - **THEN** nginx responds with `index.html` and HTTP 200 #### Scenario: Hashed asset files are served - **WHEN** a browser requests a path under `/assets/` (e.g., `/assets/index-abc123.js`) - **THEN** nginx responds with the file and HTTP 200 #### Scenario: Unknown paths fall back to index.html - **WHEN** a browser requests a path that does not match any file (e.g., `/some/route`) - **THEN** nginx responds with `index.html` and HTTP 200 ### Requirement: Gzip compression enabled nginx SHALL serve gzip-compressed responses for text-based content types. #### Scenario: JavaScript files are compressed - **WHEN** a browser requests a `.js` file with `Accept-Encoding: gzip` - **THEN** the response MUST include `Content-Encoding: gzip` #### Scenario: CSS files are compressed - **WHEN** a browser requests a `.css` file with `Accept-Encoding: gzip` - **THEN** the response MUST include `Content-Encoding: gzip` ### Requirement: Cache headers for hashed and unhashed assets nginx SHALL set long-lived cache headers for content-hashed assets and no-cache for `index.html`. #### Scenario: Hashed assets get immutable caching - **WHEN** a browser requests a file under `/assets/` - **THEN** the response MUST include `Cache-Control: public, max-age=31536000, immutable` #### Scenario: index.html is not cached - **WHEN** a browser requests `/` or `/index.html` - **THEN** the response MUST include `Cache-Control: no-cache` ### Requirement: .dockerignore excludes unnecessary files A `.dockerignore` file SHALL prevent large or irrelevant directories from being included in the Docker build context. #### Scenario: node_modules excluded - **WHEN** Docker reads the build context - **THEN** `node_modules/` directories MUST be excluded #### Scenario: .git excluded - **WHEN** Docker reads the build context - **THEN** the `.git/` directory MUST be excluded ### Requirement: Port mapping via docker-compose The `docker-compose.yml` SHALL map a host port to the container's port 80, defaulting to host port 8080. #### Scenario: Default port mapping - **WHEN** `docker compose up` is run without overrides - **THEN** the app MUST be accessible at `http://localhost:8080` #### Scenario: Custom port via environment variable - **WHEN** the user sets an environment variable or edits the compose file to change the host port - **THEN** the app MUST be accessible on the configured port