Files
2026-10-25 22:31:22 +01:00

7.5 KiB
Raw Permalink Blame History

Deployment

Both components ship as Docker images and run together through docker-compose.yml. An already existing reverse proxy handles TLS and routing. Compose therefore publishes no ports.

Topology

flowchart LR
    U[Browser] -->|HTTPS| P[Reverse proxy]
    P -->|network actionBlocks_website| W[actionblocks-website :80<br/>nginx, static files]
    P -->|network actionBlocks_server<br/>WebSocket| S[actionblocks-server :8080<br/>ASP.NET Core]
Container Image built from Port Docker network
actionblocks-website actionBlocks_website/Dockerfile 80 actionBlocks_website (external)
actionblocks-server actionBlocks_server/Dockerfile 8080 actionBlocks_server (external)

Both networks are declared external: true. They must exist beforehand and be attached to the proxy:

docker network create actionBlocks_website
docker network create actionBlocks_server

Images

Server (actionBlocks_server/Dockerfile)

  1. mcr.microsoft.com/dotnet/sdk:10.0 restores the packages, runs the unit tests (a failing test fails the build), then publishes in Release mode.
  2. mcr.microsoft.com/dotnet/aspnet:10.0 runs dotnet ActionBlocks.Server.dll as the non-root $APP_UID user on port 8080.

Website (actionBlocks_website/Dockerfile)

  1. node:22-alpine runs npm ci && npm run build (typecheck + Vite build into dist/).
  2. nginx:1.27-alpine serves dist/ with docker/nginx.conf.
  3. docker/40-runtime-config.sh is placed in /docker-entrypoint.d/. The official nginx image runs that directory's scripts at every container start. The script writes config.js from the SERVER_URL env var, so the same image works in any environment without a rebuild.

nginx behaviour (docker/nginx.conf):

Path Behaviour
/static/… Hashed bundles, cached for a year (immutable)
/assets/… Exchangeable graphics, no-cache (revalidated, so replaced files appear immediately)
/config.js, /index.html Never cached
anything else File if it exists. Otherwise the first path segment is stripped and the lookup retried (this supports proxies that do not strip a sub-path). Finally falls back to index.html

Configuration

Compose reads a .env file next to docker-compose.yml. Copy the template and edit it:

cp .env.example .env
Variable Used by Default Description
SERVER_URL website empty → <site origin>/hub Full hub URL as the browser sees it, e.g. https://game.example.com/server/hub
ALLOWED_ORIGINS server empty → any origin Comma-separated list of origins allowed to connect, e.g. https://game.example.com
PATH_BASE server empty Prefix to accept if the proxy forwards a sub-path without stripping it, e.g. /server
HUB_PATH server /hub Hub endpoint path. It is fixed to /hub in the compose file
GAME_CONFIG server /app/config/game.json (compose) Path of the gameplay config file, see below
Game__<Setting> server see GameSettings.cs Overrides a value from the config file, e.g. Game__CountdownSeconds=5 or Game__Items__blind__DurationSeconds=6. Add these under the server's environment: in the compose file

Game config

config/game.json (next to docker-compose.yml) holds the gameplay tuning. The compose file mounts the config folder read-only into the server container.

{
  "Game": {
    "ItemSpawnChance": 0.1,            // chance (0..1) per block of a new piece to carry an item
    "Items": {
      "nuke":  { "Weight": 1 },        // share = weight / sum of all weights; 0 = disabled
      "blind": { "Weight": 2, "DurationSeconds": 4 },
      ...
    }
  }
}
  • Effective chance of an item per block = ItemSpawnChance × weight / sum of weights. With the defaults (all weights 1), each of the six items gets 1/6 of the 10%.
  • DurationSeconds exists only for the timed items blind and transform. The other items act instantly.
  • Items left out of the file keep weight 1 and their built-in duration. Item ids are case-insensitive. Any other GameSettings value (e.g. CountdownSeconds) may be added to "Game" too.
  • Changes apply from the next match on, without a restart. Running matches keep the values they started with. The server logs Game config reloaded with the new shares.
  • Invalid values (chance outside 0..1, negative weight, duration ≤ 0, unknown item, duration on an instant item, broken JSON) are reported in the server log:
    • at startup, the server refuses to start;
    • on a later edit, the change is ignored and the last good config stays active.

Start or update:

docker compose up -d --build

Verify:

docker exec actionblocks-website cat /usr/share/nginx/html/config.js   # shows SERVER_URL
curl https://<your-host>/<server-path>/health                             # {"status":"ok"} (the server image has no curl/wget, so test through the proxy)

Reverse proxy requirements

  1. Route the website to actionblocks-website:80.
  2. Route the hub to actionblocks-server:8080, with WebSocket upgrade support. The Upgrade and Connection headers must be forwarded, and read timeouts must be long because the connection stays open for the whole session.
  3. If the hub runs on a different origin than the site, list the site's origin in ALLOWED_ORIGINS.

Sub-paths

Serving under paths such as https://game.example.com/website/ and /server/ is supported:

  • Website: works whether the proxy strips /website or not. All URLs in the build are relative, and nginx retries unknown paths without their first segment.
  • Server:
    • If the proxy strips /server, nothing else is needed. The hub answers at /hub.
    • If it does not strip the prefix, set PATH_BASE=/server.
  • Set SERVER_URL to the public hub URL, e.g. https://game.example.com/server/hub.

Example: nginx as the reverse proxy

# Website under /website/ (prefix stripped by the trailing slash in proxy_pass)
location /website/ {
    proxy_pass http://actionblocks-website:80/;
}

# Game server under /server/ (prefix stripped)
location /server/ {
    proxy_pass http://actionblocks-server:8080/;
    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;
}

Matching .env:

SERVER_URL=https://game.example.com/server/hub
ALLOWED_ORIGINS=https://game.example.com
PATH_BASE=

Troubleshooting

Symptom Likely cause Fix
Browser console: Refused to apply style … MIME type (''), 404 for config.js / index-*.js Assets requested from the wrong path, e.g. an old image built with absolute URLs, or the proxy doesn't route the sub-path Rebuild the website image, check the proxy route
Red banner "Server unreachable, retrying..." SERVER_URL wrong, hub not routed, or WebSocket upgrade blocked Open <SERVER_URL>/negotiate?negotiateVersion=1 with a POST (e.g. curl -X POST); it must return JSON. Check the upgrade headers on the proxy
CORS error in the console Site origin not in ALLOWED_ORIGINS Add the exact origin (scheme + host + port)
Works, then players are kicked after ~60 s idle Proxy read timeout too short Raise the proxy's timeout for the hub route
Changed .env has no effect Containers not recreated docker compose up -d (recreates containers with new env)
All lobbies vanish Server container restarted Expected: state is in memory only