There are two ways to run a Dot server, and the difference is what you want to operate.
dot-server-deploy
TMC’s server tool. YAML configuration, games loaded out of a directory, roles, moderation, votes, a browser client and a site listing. This page.
dotserve
A shell script that starts your own game’s dot-server with sensible
defaults and prints a join address. For a game you are writing.
This page assumes Godot 4.7.2 and a terminal — see Getting started if you do not have them yet.
Quick start
git clone https://github.com/modcommunity/dot-server-deploy.git
cd dot-server-deploy
./setup.sh # find a runtime, wire the addons, write ./server
./server # start it
Windows: setup.bat, then .\server.ps1, which takes the same options and
accepts -Port as readily as --port.
setup.sh never overwrites a config file that already exists, so running it
again after an update is safe: your edits are the server’s configuration.
./server |
Start it. |
./server check |
Boot, load the game, shut down. Exit 0 if it worked. |
./server config |
The resolved configuration. |
./server games |
What is in content/. |
./server pack <id> |
Publish content/<id> as a signed dot-cloud pack. --all does every one. |
./server verify <id> |
Check a published pack’s manifest against keys/content.pub. |
./server export-web |
Build the browser client, plus the loader the site uploads. |
./server export-native |
Build the native client the desktop app installs — one archive per platform. |
./server --help |
Every option. |
./server --port 27015 --name "My server"
./server -- +sv_cheats 1 +changelevel lobby
Exit codes are meaningful, so a supervisor can tell a misconfiguration from a
crash: 2 usage, 3 no runtime, 4 no project, 5 bad config, 6 port in
use, 7 bad content.
Run `./server check` before you open a port
It boots a real server, loads the game, shuts down and exits non-zero if any of that failed — the difference between “the config is wrong” and “the config is wrong and I have been telling people to connect for ten minutes”.
Looking at it yourself first
./play.sh # a server and the browser client, on the loopback
./play.sh playground # start on a particular game
./play.sh down
It prints a link. play.sh rebuilds the web export when the game is newer, puts
the page that reads ?server= in place of Godot’s generated one, and warns when
the vendored game/ directory is older than the repositories it was copied
from — the one part of the tree that goes stale, and a stale copy exports
cleanly, runs, and is last week’s game.
Loopback HTTP only
An HTTPS page may not open a ws:// socket. play.sh works on 127.0.0.1 and
nowhere else; a public deployment needs TLS in front of it.
Directory layout
cfg/ what an operator edits
content/ what the server serves
data/ what the server writes
host/ the boot: YAML -> DotServer
client/ the client shell. Knows nothing about any game
web/ the browser build, and the page that takes ?server= from the URL
cfg/ and content/ are read; data/ is written. A container mounts the first
two read-only and the third read-write, and a systemd unit points
ReadWritePaths at exactly one directory.
What each file in cfg/ and data/ is
cfg/
server.yml name, slots, tickrate, queries, the game to boot
net.yml bind address, port, bandwidth
rcon.yml remote console. Generated once, printed once
auth.yml the identity backend, or nothing and everybody is a guest
groups.yml groups, as sets of permissions
vote.yml voting for the next game: rtv, nominations, time limits
permissions.yml who is in which group
content/<id>/ per-game configuration
content/
global/ loaded by every game
lobby/game.yml the lobby. The default
data/
from_yaml.cfg what your YAML became
admins.json bans.json audit.jsonl listing.jsonA directory under content/ with a game.yml in it is a game, and
changelevel switches between them with players still connected.
`data/from_yaml.cfg` is what your YAML became
Read it first when a setting appears to be ignored. cfg/*.yml is a surface
over dot-server’s own vocabulary, not a second vocabulary — an operator writes
net_port, dot-server calls it port — and that file is the translation.
An unknown key is reported, never fatal: refusing to boot because a config mentions a setting from a newer version is worse than ignoring it.
The settings that decide whether anybody can join
- net_portnet.yml
The game port,
6070out of the box. This is what players connect to and what your firewall has to allow.- net_bind_ipnet.yml
127.0.0.1out of the box, which is correct behind a reverse proxy and wrong for a server the internet should reach directly. Set it to0.0.0.0or to the interface you mean.- net_public_ipnet.yml
Only needed behind NAT. It goes into the join line and the listing; nothing binds to it.
- query_bind_ipserver.yml
Where the query listener sits, separately from the game. A reverse proxy forwards a WebSocket and cannot forward UDP, so a query listener that inherited
net_bind_ipwould be listening where nothing can ask it — and a tracker cannot tell that from a server that is down.*puts the query port, and only the query port, on every interface. RCON does not follow it.- a2s_enabledserver.yml
Answers the twenty-year-old query protocol beside Dot’s own, on the same socket. The trackers, uptime monitors and server browsers that already exist speak A2S and nothing else — including this site’s scanner.
Both protocols come from dot-server-query, which the server tool installs for you. A build without it answers nothing on the query port at all, and a server nothing can ask is a server nothing can list.
- sv_name / sv_maxplayersserver.yml
What the listing and the server browser show, and how many seats there are.
Permissions
dot-server’s model is flags. groups.yml is the translation into roles, so
an operator writes a role and a player gets the flags:
groups:
admin: [kick, ban, mute, changemap, chat, vote]
users:
gamemann: # by name, for somebody who plays here
group: owner
backbone:clx8f2k0kd: # by uid, for somebody who does not
group: admin
A key is matched against a player’s uid, their username and their display name, in that order, lower-cased.
An unrecognised flag grants nothing, silently
warn, announce and change were all written here in good faith and matched
nothing, so a group meant to run the server could kick, ban and mute and could
not change the map, use admin chat, or be recognised as staff. Nothing errored,
because an unknown flag is exactly what a game’s own flag looks like.
The flags that exist are listed at the top of groups.yml, and unknown ones are
named at boot. Read that line.
`permissions.yml` does nothing without authentication
Permissions are refused to any unauthenticated session, because a guest uid is a random per-device string and granting anything to one grants it to anyone. That is correct, and it looks exactly like the file being ignored.
Letting players choose what runs next
cfg/vote.yml puts dot-vote over the games in
content/:
!nominate !rtv !votefor !timeleft !nextmap
Each game gets its own time limit in its own game.yml, because forty minutes
of surf and ten minutes of a lobby deathmatch are not the same number.
enabled: false turns the whole thing off.
vote.yml is the one file that is not a console surface — it is dot-vote’s
DotVoteRules applied straight onto the resource, so every setting is
documented on the property of the same name, and enums are written by name
(method: instant_runoff, not method: 2).
RCON
RCON ships off; cfg/rcon.yml says how to turn it on, and the password is
generated once and printed once.
--rcon-password is refused outright: argv is readable by every other process
on the machine and ends up in pasted bug reports, which is why configuration
refuses secrets from argv and the environment too. In the container RCON is
published on loopback only — reach it through an SSH tunnel, or widen the
mapping deliberately with an address allow-list in cfg/rcon.yml.
In a container
docker compose up -d
docker compose logs -f
The build context is the parent directory: every addon is its own repository
and there is no way to clone the tree at once, so they are siblings rather than
subdirectories. cfg/ and data/ are bind mounts, so the first run writes a
configuration you can edit and an RCON password that survives a rebuild;
content/ is mounted read-only. The container runs as your own uid
(UID=$(id -u) GID=$(id -g) docker compose up -d) and drops every capability.
Getting it onto the site
Two halves, and they are independent.
The listing you create on the site — see
Getting listed and claiming. The scanner queries the
address you give it, which is why a2s_enabled and query_bind_ip matter.
Reporting is the server telling its listing what it is. It needs
data/listing.json with a server-scoped integration token:
{
"backbone_url": "https://moddingcommunity.com",
"integration_token": "…",
"report_interval_sec": 60
}
With no token nothing runs and the server is simply not listed, which is correct
for a LAN game and for every test. The token is a path, never a value:
integration_token is refused from the environment and from argv, and the file
is per-server, so several servers sharing one cfg/ each have their own
listing.
Why a server reports at all, when it could be queried
Three things break a query and only one is fixable from the server: a server behind a reverse proxy has its game port on loopback and a proxy cannot forward UDP; a scanner will not query a private address, and is right not to, so a LAN deployment is unscannable by design; and a query packet is lossy, rate-limited and truncates a player list.
A server that reports itself also knows two facts a packet cannot carry: how many of its players are bots, and which games it can run — which for a multi-game server is the interesting half, because the game it happens to be running this minute is not what it is.
The chat box on the site
DotChatRelay joins your server’s chat to its room on the website: what players
type reaches the page, what members type reaches the game, and a line beginning
with / or ! can run as a console command. It needs three things, in order.
Setting up the relay
1. A credential. The same data/listing.json as above. With no token there
is no backbone client, and the relay refuses to start rather than polling a URL
it cannot authenticate to.
2. The relay turned on.
DOT_CHAT_RELAY_ENABLED=1 # carry chat both ways
DOT_CHAT_RELAY_ALLOW_COMMANDS=1 # let `/something` run as a console command
DOT_CHAT_RELAY_COMMAND_SOURCE=2 # 2 is RCON; 3, the default, is CHATThe source is the decision. At CHAT a relayed command reaches only what a connected player could type — and several games deliberately withhold a map change from that, because a map change destroys every run in progress. At RCON it reaches what an operator at a remote console reaches, which is what “my site admins are administrators of this server” actually means. Either way the uid is resolved from the site author and the permission answer is still your own files.
3. One uid in cfg/permissions.yml. A site member’s uid is backbone: plus
a database id that appears on no page, so use it once and read the log: type
/map into the server’s chat box on the website and the refusal names the exact
key to add.
There is deliberately no way to grant it by the display name shown beside the message: a display name is a string the person can change on their own profile page, and a permission keyed on one is a permission anybody can take by renaming themselves.
Once the relay is up the server posts its command table to the site, and the
chat box offers those commands when a member types /. The list is built at the
relay’s own source, so what the menu shows is what that person could actually
run.
Known limits
- One transport at a time. A server listens on WebSocket or ENet, so a desktop client on UDP and a browser client on TCP cannot share a match yet.
- No TLS of its own. A page on HTTPS cannot open
ws://. Certificates and a reverse proxy are deployment, not code. cfg/permissions.ymldoes nothing without authentication — see above.- No game has been delivered as a pack yet. The games ship inside the build.
