Skip to content

Containerized stack: production frontend service in docker-compose.test.yml - #83

Merged
vladmesh merged 2 commits into
mainfrom
claude/compose-frontend-service-ktivtm
Sep 29, 2026
Merged

vladmesh merged 2 commits into
mainfrom
claude/compose-frontend-service-ktivtm

Conversation

@vladmesh

@vladmesh vladmesh commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Card W3-10 (part of issue containerized-stack).

Starting point

On main (2f79530), docker-compose.test.yml had only backend and integration-tests. The web UI could only run through the Vite dev server. This item was still open on main.

Change

  • frontend/Dockerfile: multi-stage build. A node:22-alpine stage runs npm ci && npm run build, which includes tsc -b. An nginx:1.27-alpine stage serves dist/. The image has a HEALTHCHECK.

  • frontend/nginx.conf.template: rendered at start-up by the nginx image's envsubst step. Only ${BACKEND_URL} is substituted (default http://backend:8001). It mirrors vite.config.ts:

    • /api/ is proxied to the backend with WebSocket upgrade headers (Upgrade/Connection map, HTTP/1.1). Host is preserved, like Vite's default changeOrigin: false. Read/send timeouts are 1h so idle game sockets stay open.
    • = /health is proxied.
    • / uses try_files … /index.html, so BrowserRouter deep links work.
    • /assets/ returns 404 for missing files instead of serving the SPA shell, and caches hashed bundles as immutable.
  • docker-compose.test.yml adds two services, both behind profiles: ["frontend"]:

    • frontend: healthcheck, depends_on: backend (service_healthy), published on ${FRONTEND_PORT:-8080}.
    • frontend-smoke: reuses the Dockerfile.test image and mounts tests/frontend_proxy/.

    Because of the profile, docker compose config --services without it is still backend, integration-tests. make test-integration does not build or start the frontend and is no slower.

  • make test-frontend-container (new, separate target): brings up backend, frontend and frontend-smoke. The exit code comes from frontend-smoke. It then runs down.

  • .dockerignore: the root file (backend context) gets a comment that it already excludes frontend/. A new frontend/.dockerignore excludes node_modules, dist and editor files.

  • Docs: README gets a "Containerized stack" subsection, plus updated requirements and the command list. CLAUDE.md gets the command list and a log-to-file example.

Tests

tests/frontend_proxy/test_frontend_proxy.py has 9 tests, all going through the nginx container:

  • index serves the production build, not /src/main.tsx
  • a hashed JS asset is served
  • the SPA fallback works for /master
  • a missing asset returns 404
  • /health is proxied
  • /api/master/worlds returns the same result through the proxy as from the backend directly
  • API 404 JSON passes through
  • WebSocket upgrade works for an unknown session (error frame received)
  • a full player WebSocket session: session and character are created via the proxy, then a turn is received and end_turn is sent

These are outside testpaths, so make test does not pick them up.

Fails on main: yes. With main's compose file, docker compose --profile frontend up … frontend frontend-smoke fails with no such service: frontend, and main has no test-frontend-container target.

What was run

  • make check: green. 2939 pytest, 353 vitest, ruff, mypy, eslint and tsc all pass.
  • Frontend smoke: 9 passed, exit 0.
  • make test-integration with the new compose file: 184 passed, and no frontend container started.
  • docker compose config is valid with and without the profile.

Not run as-is: the literal make test-frontend-container / make test-integration with --build. In the dev VM, ghcr.io blobs (the uv image) are blocked. Containers also can't reach PyPI or npm without the sandbox's proxy CA. I built the three images locally with --network host and the proxy CA injected through a scratch copy of each Dockerfile; the repo Dockerfiles were not changed. I then ran the same compose services with --no-build. The build steps themselves, including npm ci and npm run build in the node stage, succeeded that way.

Found, not fixed (out of scope)

  • make test-integration never runs docker compose down, so containers and the network are left behind between runs. The new target does clean up.
  • The compose stack runs the backend as ${UID:-1000}. If UID/GID aren't exported (for example when calling docker compose directly from bash, where UID is a non-exported shell variable), the backend can't create saves/<world> in a root-owned ./saves, and autosave logs PermissionError. This is not fatal to the tests.
  • The compose stack is test-oriented: integration test content and DND_DICE_SEED=42. A production compose file using the real content/, persisted saves and no fixed seed would be a separate item.
  • CI does not run the frontend container smoke test. Adding a job for it, for example on changes under frontend/ or docker*, is a follow-up decision.

frontend/Dockerfile builds the SPA in a node stage and serves it with
nginx, which proxies /api (REST + game WebSocket) and /health to
$BACKEND_URL like the Vite dev proxy, with SPA fallback for client-side
routes and a healthcheck. The compose service sits behind the
"frontend" profile so make test-integration never builds it;
make test-frontend-container runs tests/frontend_proxy/ against it.
@vladmesh

Copy link
Copy Markdown
Owner Author

Independent review: PASS

  • What changed (card W3-10):
    • A new frontend service in docker-compose.test.yml: multi-stage frontend/Dockerfile, where node builds the app and nginx serves it.
    • nginx.conf.template mirrors the Vite proxy: /api/ with WebSocket upgrade, /health, an SPA fallback, and /assets/ returning 404 for missing files.
    • The new service and the frontend-smoke service sit behind profiles: ["frontend"], so make test-integration and CI are unaffected.
  • Checks:
    • Local make check is green and required CI is green.
    • I built and ran the stack locally with Docker via make test-frontend-container: 9/9 smoke tests passed through the nginx proxy, including a full player WebSocket turn loop. The containers were cleaned up afterwards.
  • Regression check: there is no behaviour on main to regress; main has no frontend service or target.
  • Non-blocking: CI does not run this smoke test. Wiring test-frontend-container into a workflow is a reasonable follow-up.

@vladmesh
vladmesh merged commit 6e07d24 into main Sep 29, 2026
4 checks passed
@vladmesh
vladmesh deleted the claude/compose-frontend-service-ktivtm branch September 29, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant