Files
Josh Creek 4ffa1543cc docs: design per-match server autoscaling, with measured boot time
Casual/ranked queues need servers allocated per match and shut down
afterwards, so cost is incurred only while a match runs - while the
existing Docker and CI gates keep passing unchanged.

Measured against the repo's own cosmicclash-server image rather than
estimated: the runtime image is ~148 MB of content, and boot to the
server_started line is ~870 ms on the container's own clock. That was
taken under x86_64 emulation on an arm64 host, so it is a pessimistic
bound and is recorded as one - it needs re-measuring on native Linux
before it sets any timeout.

Two findings that would each break a naive implementation, both hit
while taking that measurement:

- Godot's stdout is block-buffered off a TTY. A detached container logs
  nothing at all - server_started does not appear even after 35s - so an
  orchestrator readiness probe that greps the log hangs forever. Probe
  the UDP socket or flush explicitly.
- --port defaults to 7777 and the Dockerfile hardcodes EXPOSE 7777/udp,
  so several matches cannot share a host without a port range or an
  address per match. Being UDP, L7 ingress routing does not apply.

Also records the honest tension in 'only pay during a match': a server
must listen before players connect, and image pull plus scheduling can
dwarf 870 ms, so the recommendation is match-level scale-to-zero over a
small warm node pool rather than node-level scale-to-zero.

The rule for keeping verify-phase6 and verify-enet-integration green:
every allocation feature is opt-in via a ServerConfig flag defaulting to
current behaviour, with a second Compose file rather than mutating
compose.phase6-smoke.yml.
2026-08-31 18:45:05 +01:00

99 lines
3.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dedicated server
Phase 6 packages a self-hosted ENet server. It does not publish an image or
binary: build from this checkout and run the generated image locally or on a
VPS. Direct-IP ENet uses UDP only; the default port is `7777`.
## Local build and verification
Docker is the primary path. It builds the stripped `Linux Dedicated Server`
export, runs it in one container, joins two independent headless clients from
two other containers, forces one server-owned goal in each of two matches,
checks both clients observed both scores and arena rotation, then drains.
```bash
make verify-phase6
```
The command prints the temporary log directory even on failure and always
removes its Compose containers. It neither pushes an image nor uploads an
artifact. The same command is the only operation in the Phase 6 GitHub Actions
workflow. Its pinned Godot build image is about 2.4 GB, so leave several GB of
Docker disk space free for its layers and the exported project.
To build and run a server manually:
```bash
docker build --target server -t cosmic-clash-server .
docker run --rm -p 7777:7777/udp cosmic-clash-server \
--port=7777 --min-players=2 --start-countdown=5
```
All server output is structured stdout/stderr. Use Docker's logging driver for
rotation; for example, configure `json-file` with `max-size` and `max-file` on
the host. Do not add in-process log rotation.
## Configuration
Every flag is printed by `--help`; unknown flags fail startup. Command-line
values override a Godot config file's `[server]` values, which override
defaults. Mount one into the container when needed:
```ini
[server]
port=7777
max-clients=12
min-players=2
start-countdown=5
arena-rotation=sequential
log-level=info
```
```bash
docker run --rm -p 7777:7777/udp \
-v "$PWD/server.cfg:/etc/cosmic-clash/server.cfg:ro" \
cosmic-clash-server --config=/etc/cosmic-clash/server.cfg
```
`--max-matches=N` drains only after match `N` ends, then exits `0`; use it for
planned restarts under a process supervisor. `--smoke-force-goal-after=<seconds>`
is a documented local-verification switch; its default `-1` disables it, and it
must not be used for normal matches.
## Native systemd deployment
Copy the exported binary and assets to `/opt/cosmic-clash`, create the
`cosmicclash` service user, place configuration at
`/etc/cosmic-clash/server.cfg`, then install
`deploy/cosmic-clash-server.service` as
`/etc/systemd/system/cosmic-clash-server.service` and enable it:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now cosmic-clash-server
sudo journalctl -u cosmic-clash-server -f
```
Godot does not provide a GDScript SIGTERM hook. `systemctl stop`, Ctrl-C, or a
container stop terminates immediately and connected ENet clients will time out
after roughly five seconds. Prefer `--max-matches` for planned drains.
## Network and sizing
Open and forward **UDP 7777** (or the configured `--port`) in the host firewall
and any cloud security group. TCP is not used. The Phase 1 sizing estimate is
roughly 610 simultaneous match processes per modern core, 150250 MB RSS per
process, and about 630 kbit/s upstream for a full six-player match; use those
as a starting point and monitor actual CPU, RSS, and egress.
Per-match autoscaling — allocating a server for one match and shutting it
down afterwards — is designed in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md)
and not yet implemented. The sizing numbers above predate that work and
should be re-measured under real concurrency before they size a bill.
This build must not be exposed to strangers yet. Slot reclaim is still keyed
by display name, so a player who knows a disconnected player's name can claim
their reserved slot. Phase 7 Steam-auth identity is the required fix. Local,
LAN, and controlled VPS verification are in scope; the public-internet phase
gate remains blocked on that identity work.