Files
emba-course-solver/openspec/changes/docker-compose-deployment/specs/docker-deployment/spec.md
Bill Ballou 663d70e44b Add Docker Compose deployment with multi-stage build and nginx
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`.
2026-02-28 23:01:50 -05:00

3.7 KiB

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