Serve the frontend from docker-compose via nginx

Milestone 9 originally planned Caddy as the reverse proxy handling
both static serving and TLS. That's changed: the user has their own
external nginx that will handle SSL/certs and route to this app once
it's exposed beyond the LAN — so this compose stack only needs to
serve the frontend over plain HTTP for now, no TLS layer of its own.

Rewrote frontend/Dockerfile from the old scratch/dist-only build
(meant to hand its output to Caddy) into a self-contained nginx image:
builds the Vite app, then serves it from nginx on port 80. Added
frontend/nginx.conf, which also reverse-proxies /api, /auth, /users,
/ws, and /health to the backend container — necessary because
api/client.ts calls relative paths, so the SPA and API need to appear
as one origin to the browser (same pattern the Vite dev-server proxy
already used, now the production equivalent). The /ws location sets
the Upgrade/Connection headers for the WebSocket handshake and a long
proxy_read_timeout, since DM turns can take well over nginx's 60s
default before the connection sees more traffic.

docker-compose.yml gets a frontend service, port 8080:80.

Verified: docker compose up -d --build frontend; confirmed the built
SPA loads, /api and /health proxy through to the backend (401/200,
not 502), and — the part most likely to break — a real WebSocket
opened successfully through the proxy and a full message round-trip
(send -> persist -> broadcast -> typing -> DM turn) worked end to end
against the containerized stack, not just the Vite dev server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thorsten
2026-09-02 16:27:23 +02:00
parent 7bbdee868a
commit b75cfb1437
4 changed files with 65 additions and 4 deletions
+48
View File
@@ -0,0 +1,48 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Same-origin as far as the browser is concerned — api/client.ts calls relative paths, so
# this proxies them to the backend container internally instead of relying on CORS.
location /api/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /auth/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /users/ {
proxy_pass http://backend:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /health {
proxy_pass http://backend:8000;
}
location /ws/ {
proxy_pass http://backend:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
# SPA fallback — client-side routes (e.g. /games/<id>) must resolve to index.html.
location / {
try_files $uri /index.html;
}
}