Dedicated Server Hosting
Production layout and startup contract
Direct packaging, Docker, and systemd use the same staged layout:
/opt/domination/server/config/server.json
/opt/domination/server/config/release-maps.json
/opt/domination/server/src/*.js
/opt/domination/unity/Assets/StreamingAssets/data/maps/<approved-id>.json
/server-data/ # the only mutable tree
server.json is required. Relative paths inside it resolve from the config file, never from the
process working directory. release-maps.json is the versioned allow-list for built-in maps; every
listed descriptor must exist, parse, match its ID, and provide at least two spawns. The server exits
before listening if the config, manifest, map directory, any approved descriptor, or writable data
tree is unavailable. Approved custom maps under /server-data/maps/approved are additive and cannot
replace a built-in map ID.
The current catalog is 2026.07.12 with nine approved skirmish maps. Campaign and test descriptors
are not advertised by production servers.
Validate a checkout
The server uses Node.js built-ins only (Node 20 or newer). From the repository root:
npm --prefix server run validate
SERVER_DATA_DIR="$(mktemp -d)" npm --prefix server run validate:deployment
npm --prefix server run test:deployment
validate:deployment loads the required config and complete release catalog without opening a
socket. A healthy result includes:
"event":"deployment_validated" ... "releaseMapCount":9
Secure transport contract
Public and LAN runtime binds require native TLS 1.2 or newer. The server reads a PEM certificate chain and private key before it creates runtime directories or opens a socket, and fails closed if either is absent, unreadable, malformed, or mismatched:
DEDICATED_SERVER_HOST=0.0.0.0 \
TLS_CERT_PATH=/etc/domination/tls/fullchain.pem \
TLS_KEY_PATH=/etc/domination/tls/privkey.pem \
SERVER_DATA_DIR="$HOME/.local/share/domination-server" \
node "$(pwd)/server/src/server.js" \
--config "$(pwd)/server/config/server.json"
The certificate SAN must contain the hostname players enter. Unity shipping
clients use platform chain, lifetime, revocation, and hostname validation;
players connect with tls://game.example.com:7777 (a bare hostname also
defaults to TLS). This is TLS-wrapped newline JSON, not HTTP. The automated
player harness has a separate exact-certificate pin that activates only for
MP_AUTOTEST=host|join after the connected peer is confirmed as loopback;
interactive and public connections cannot activate it.
Plaintext is a local development exception only. It requires an explicit flag
on both processes and literal numeric loopback addresses—hostnames including
localhost, LAN addresses, and public addresses are rejected:
ALLOW_INSECURE_LOOPBACK=1 DEDICATED_SERVER_HOST=127.0.0.1 \
node server/src/server.js --config "$PWD/server/config/server.json"
domination_ALLOW_INSECURE_LOOPBACK=1 \
MULTIPLAYER_SERVER_URL=tcp://127.0.0.1:7777 \
./unity/Build/domination/domination.x86_64
Never use account, lobby, or reconnect credentials in plaintext mode.
For a local server, pass the config explicitly. The absolute path keeps the invocation independent of the shell's current directory:
SERVER_DATA_DIR="$HOME/.local/share/domination-server" \
node "$(pwd)/server/src/server.js" \
--config "$(pwd)/server/config/server.json"
The checked-in launcher resolves its own source path and accepts the same arguments:
server/domination-server --config "$(pwd)/server/config/server.json" --port 7777
Readiness
The TLS-authenticated ping response is safe for health monitoring and
contains no secrets:
{
"type": "pong",
"ready": true,
"configLoaded": true,
"config": "server.json",
"catalogVersion": "2026.07.12",
"releaseMapCount": 9,
"mapCount": 9
}
Run the shipped probe from the staged server package:
READINESS_HOST=127.0.0.1 READINESS_PORT=7777 \
READINESS_SERVER_NAME=game.example.com \
node /opt/domination/server/scripts/readiness-check.mjs
Public-CA certificates use Node's platform trust store. For a private CA or
the checked-in localhost test fixture only, also set
READINESS_TLS_CA_PATH=/path/to/ca.pem. The probe never disables certificate
or hostname validation.
mapCount can be greater than releaseMapCount when approved custom maps are installed. Readiness
requires at least one release map and persistenceState: "healthy", so custom content cannot hide a
broken production map mount and unresolved result recovery cannot accept new matches.
Docker
Build from the repository root. The root .dockerignore sends only server runtime files and map
descriptors; the packager copies only manifest-approved maps into the final image.
docker build -f server/Dockerfile -t domination-server:2026.07.12 .
docker volume create domination-server-data
docker run -d \
--name domination-server \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=16m \
--cap-drop ALL \
--security-opt no-new-privileges:true \
-p 7777:7777/tcp \
-v domination-server-data:/server-data \
-v /srv/domination/tls:/certs:ro \
-e TLS_CERT_PATH=/certs/fullchain.pem \
-e TLS_KEY_PATH=/certs/privkey.pem \
-e READINESS_SERVER_NAME=game.example.com \
domination-server:2026.07.12
The image runs as the unprivileged node user. Config, code, manifest, and map
descriptors live in the read-only image; only /server-data is writable. TLS
material is a read-only bind mount. The built-in Docker health check runs the
same authenticated readiness probe and the container fails closed when the
certificate/key are absent:
Ensure the mounted certificate and private key are readable by container UID
1000 without making the private key world-readable (for example, owner/group
1000 with mode 0640). For a private CA, mount its PEM too and set
READINESS_TLS_CA_PATH to the in-container path.
docker inspect --format '{{json .State.Health}}' domination-server
docker logs domination-server
For a host bind mount instead of the named volume, create it for container UID/GID 1000 first:
sudo install -d -o 1000 -g 1000 -m 0750 /srv/domination/server-data
Do not mount over /opt/domination; doing so removes the validated config or release catalog and
startup will fail intentionally.
Linux/systemd
From a repository checkout, the installer creates the unprivileged domination account, stages the
same manifest-selected runtime used by Docker, creates /server-data, installs the hardened unit,
validates the deployment in ExecStartPre, and starts it:
sudo TLS_CERT_SOURCE=/etc/letsencrypt/live/game.example.com/fullchain.pem \
TLS_KEY_SOURCE=/etc/letsencrypt/live/game.example.com/privkey.pem \
TLS_SERVER_NAME=game.example.com \
server/scripts/install-systemd.sh --start
sudo journalctl -u domination-server -f
To inspect before starting, omit --start, then run:
sudo systemctl enable --now domination-server
sudo systemctl status domination-server
sudo env READINESS_SERVER_NAME=game.example.com \
node /opt/domination/server/scripts/readiness-check.mjs
The installer copies the supplied certificate and key to
/etc/domination/tls, writes a root-owned service environment file, and runs
catalog, key-pair, and certificate-hostname preflight before replacing or
starting the unit. TLS_SERVER_NAME must be the hostname players use. For a
private CA, also supply TLS_CA_SOURCE=/path/to/ca.pem; public-CA deployments
use Node's platform trust store. Rerun the installer after certificate
rotation. The unit treats /opt/domination and TLS
material as read-only and grants writes only to /server-data. Never place the
production config under /server-data; mutable state must not be able to
replace the release catalog or server policy.
domination-server.service uses Type=simple: systemctl status reports process liveness, not
application readiness. A server with retained recovery evidence deliberately keeps its diagnostic
socket alive but answers ready:false. The installer runs the readiness probe when invoked with
--start; production monitoring must run the same probe continuously and alert on a nonzero exit.
Do not route players to an instance based on active (running) alone.
Configuration overrides
The supported deployment variables are:
DEDICATED_SERVER_HOST
DEDICATED_SERVER_PORT
SERVER_CONFIG # use an absolute production path
SERVER_DATA_DIR
MAP_SOURCE_DIR # must contain every manifest-approved descriptor
MAP_MANIFEST # must be a valid, non-empty release manifest
GAME_VERSION
TLS_CERT_PATH # PEM certificate/full chain
TLS_KEY_PATH # PEM private key; never log or commit
TLS_SERVER_NAME # exact public certificate hostname for preflight
MAX_PENDING_TLS_HANDSHAKES # default 64; 1-4096 pre-auth sockets
ALLOW_INSECURE_LOOPBACK # local numeric-loopback tests only
RECONNECT_GRACE_SECONDS
DISCONNECT_OUTCOME
LOADING_TIMEOUT_SECONDS
MESSAGE_RATE_PER_SEC
MESSAGE_BURST
DESYNC_VOID_TICKS
LOBBY_CREATES_PER_WINDOW
Run --validate-only after any override and before restarting production.
Catalog-only --validate-only does not open a listener; a real runtime start
must additionally load the TLS material successfully. Use
--validate-only --validate-transport with TLS_CERT_PATH and TLS_KEY_PATH
to perform the exact systemd preflight without opening a listener.
Transport validation
The server suite covers fail-closed public startup, plaintext and hostname-based opt-in probes, hostname mismatch, malformed credentials, secret-free logs, and saturated/stalled pre-auth TLS handshakes:
npm --prefix server test
After deployment integration supplies the certificate paths and TLS-aware health probe, validate the exact public name and capture a login/lobby session:
openssl s_client -connect game.example.com:7777 \
-servername game.example.com -verify_return_error </dev/null
The capture must contain TLS records and no readable passwords, bearer or reconnect tokens, lobby credentials, chat, commands, or match state.
Lockstep timing contract
Protocol 0.1 uses a fixed deterministic simulation rate of 10 Hz. The authoritative command lead
defaults to 3 ticks and accepts only 1–20 ticks. server/config/server.json deliberately omits
commandLeadTicks, retaining the code-owned default. Invalid timing stops startup before a socket
opens.
The server sends the timing contract in every initial and pre-simulation loading reconnect config.
Clients reject missing or incompatible values and calculate their complete simulation horizon as
serverTick + commandLeadTicks - 1. Running-match reconnect is rejected until authoritative
snapshot recovery is available; command history alone is not a state checkpoint.
Safe shutdown
Use SIGTERM/SIGINT:
sudo systemctl stop domination-server
docker stop domination-server
The server sends serverShutdown to connected clients and flushes its append-only logs under
/server-data/logs before exiting.