What the server process is, and what it listens on
OpenTTD has no separate server download. The openttd.6 man page documents -D [host][:port] as the switch that starts a dedicated server, -f to fork it into the background, -g to load a savegame or scenario at start, and -x to skip writing the config on exit. The 15.3 download page lists one Linux build, a generic x86_64 archive, and states that OpenTTD needs a separate base graphics set. bootstrap_gui.cpp shows that holds for a server too: with no window to offer the download in, it stops with the error that it failed to find a graphics set. The port numbers below are the defaults in network/core/config.h in the 15.3 source; which listeners open, and when, is in network.cpp.
| Port | Direction | What it carries |
|---|---|---|
| 3979 TCP and UDP | Inbound | The game itself; UDP also answers server queries |
| 3977 TCP | Inbound | Admin network, listening only once admin_password or authorized keys are set (network.cpp) |
| 3976 TCP | Outbound | Game Coordinator, for public and invite-only servers |
| 3975 and 3974 TCP | Outbound | The STUN and TURN servers the coordinator falls back on |
docs/game_coordinator.md explains the listing. With server_game_type set to public or invite-only, the server registers with the Game Coordinator, which probes the game port over the public IPv4 and IPv6 the server announces and hands out an invite code beginning with +. A client tries a direct connection first, then STUN, then a TURN relay run by openttd.org; with a public IP and 3979 open, we expect the direct path to work.
Sizing: the tick budget, clients and memory
OpenTTD publishes no CPU or RAM minimum for a server, so size from what its docs do measure. docs/logging_and_performance_metrics.md sets the target at 30 ms per game tick, 33.33 frames a second, and the fps console command prints the per-tick timings it calls useful on dedicated servers. The game loop figure covers pathfinding, world updates and the economy; the link graph that drives cargo distribution usually runs in a background thread. Our reading, not the project's: everything else lands on one main thread, so one fast core matters more than many, and a map full of trains is what eats the 30 ms.
- Bandwidth:
docs/multiplayer.mdputs one client at about 1.5 KB/s up and down, and saysframe_freqset to 1 brings that to roughly 1 KB/s. - Players:
network_settings.inidefaults aremax_clients = 25andmax_companies = 15. - Packet memory: the multiplayer manual gives max_clients × max_clients × bytes_per_frame × 10 KiB as the most queued packets can take. With the defaults (25 clients,
bytes_per_frame = 8) our arithmetic gives about 49 MiB. - Clients:
docs/desync.mdexplains that every client runs the deterministic simulation itself and only receives commands. A slow player computer lags on a big map however fast the server is.
Install: account, archive, base set, firewall
As your own sudo user on Ubuntu 24.04, this creates an openttd system account with no login shell, a data folder at /srv/openttd (not the account's home, because docs/directory_structure.md says OpenTTD does not scan a working directory that is your home), turns the firewall on before anything listens, then fetches both archives and installs them only if their SHA-256 matches the value on each download page:
sudo apt update && sudo apt install -y curl xz-utils unzip ufw
sudo useradd --system --create-home --home-dir /home/openttd --shell /usr/sbin/nologin openttd
sudo install -d -o openttd -g openttd -m 750 /srv/openttd /srv/openttd/baseset
# firewall: IPv6 rules on, SSH, enable, then the game port
sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw allow OpenSSH
sudo ufw --force enable
sudo ufw allow 3979/tcp && sudo ufw allow 3979/udp
# OpenTTD 15.3 and OpenGFX 8.0, installed only when both checksums match
d=$(mktemp -d) && cd "$d" \
&& curl -fLO https://cdn.openttd.org/openttd-releases/15.3/openttd-15.3-linux-generic-amd64.tar.xz \
&& curl -fLO https://cdn.openttd.org/opengfx-releases/8.0/opengfx-8.0-all.zip \
&& printf '%s %s\n' f49eb25d61b00f8f4d332fee02b530ad75552d1efb8f2bb01e7ca5e6540fe059 openttd-15.3-linux-generic-amd64.tar.xz 43a0c1dabf39cb865394f3a6cc36d4da5c10ecfaaf55652043104806810903be opengfx-8.0-all.zip | sha256sum -c - \
&& sudo install -d /opt/openttd \
&& sudo tar -xJf openttd-15.3-linux-generic-amd64.tar.xz -C /opt/openttd --strip-components=1 \
&& sudo unzip -o opengfx-8.0-all.zip -d /srv/openttd/baseset \
&& sudo chown -R openttd:openttd /srv/openttd && echo installed
Go on only if it printed installed. The zip holds a single opengfx-8.0.tar, which stays packed: the directory guide says OpenTTD reads uncompressed tar files in place. Reading the 15.3 binary's dependencies, we found only base system libraries besides the FluidSynth bundled in its lib folder. Port 3977 gets no rule, so it stays closed even if you set an admin password later.
First run and the three config files
The directory guide lists the working directory first in the search order for openttd.cfg, and fileio.cpp puts private.cfg and secrets.cfg beside whichever openttd.cfg is found, with saves under the same folder. So an empty file in /srv/openttd pins everything there. The guide also says OpenTTD creates its config when it closes, and openttd.cpp writes it after the main loop ends, so a short first run fills in all three files:
sudo -u openttd touch /srv/openttd/openttd.cfg && sudo -u openttd bash -c 'cd /srv/openttd && timeout -s INT 120 /opt/openttd/openttd -D'; sudo test -s /srv/openttd/secrets.cfg && echo first run ok. It generates a throwaway map, takes SIGINT after two minutes and saves its settings. Go on only on first run ok.- Set an rcon password: `pw=$(openssl rand -hex 16) && [ -n "$pw" ] && sudo -u openttd sed -i "s/^rcon_password = .*/rcon_password = $pw/" /srv/openttd/secrets.cfg && sudo grep -qx "rcon_password = $pw" /srv/openttd/secrets.cfg && echo "rcon password: $pw"
. Note it down. For the instantsedruns, the password is visible inps` to other local users. - List it, pause it when empty and save on stop: `sudo -u openttd sed -i -e 's/^server_game_type = .*/server_game_type = public/' -e 's/^min_active_clients = .*/min_active_clients = 1/' -e 's/^autosave_on_exit = .*/autosave_on_exit = true/' /srv/openttd/openttd.cfg && [ "$(sudo grep -cxE 'server_game_type = public|min_active_clients = 1|autosave_on_exit = true' /srv/openttd/openttd.cfg)" = 3 ] && echo settings ok
. Useinvite-onlyinstead ofpublicto keep it off the list. Neither is a lock: with 3979 open, any player who finds the address can join unlessserver_passwordis set insecrets.cfg`. - Name it:
sudo -u openttd sed -i 's/^server_name = .*/server_name = Your server name/' /srv/openttd/private.cfg. Without a name,network.cppfalls back to Unnamed Server.
network.cpp pauses the game while fewer than min_active_clients players sit in a company (spectators do not count), and pause_on_join, on by default, pauses it while someone downloads the map. secrets.cfg also stores server_password for a private game and the coordinator's invite code and its secret (network_secrets_settings.ini), so the 750 folder mode matters. Because the server writes its config back on every exit, edit these files only while it is stopped.
A systemd unit that picks up the newest save
Started bare, -D generates a new map each time (video/dedicated_v.cpp), so a restart would lose the world. This small start script of ours loads the newest file in save/autosave with -g and only starts fresh when there is none. gui_settings.ini sets autosaves every 10 minutes by default, keeping 16.
sudo tee /usr/local/bin/openttd-start.sh >/dev/null <<'EOF'
cd /srv/openttd || exit 1
f=$(ls -t /srv/openttd/save/autosave/*.sav 2>/dev/null | head -n 1)
if [ -n "$f" ]; then exec /opt/openttd/openttd -D -g "$f"; fi
exec /opt/openttd/openttd -D
EOF
sudo tee /etc/systemd/system/openttd.service >/dev/null <<'EOF'
[Unit]
Description=OpenTTD dedicated server
Wants=network-online.target
After=network-online.target
[Service]
User=openttd
Group=openttd
WorkingDirectory=/srv/openttd
ExecStart=/bin/bash /usr/local/bin/openttd-start.sh
TimeoutStopSec=120
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now openttd
journalctl -u openttd -f
dedicated_v.cpp catches SIGTERM, which is what systemctl stop sends, and writes exit.sav into the autosave folder when autosave_on_exit is on; TimeoutStopSec=120 gives a large map time to finish that. Restart=always brings it back after a crash. The man page says -D sets network debug output to level 6, so the journal is chatty, and network_coordinator.cpp logs the invite code there once the server registers. The unit has no console on stdin: run commands through rcon from a game client, as the wiki's dedicated server page shows, with rcon yourpassword "kick 5" in the client console. To start from a map you built at home (saved by the server's OpenTTD version or an older one), stop the service, copy the .sav into /srv/openttd/save/autosave/, chown it to openttd, run sudo touch on it because scp -p and rsync -a keep the old timestamp, and start again: the newest file wins. If the newest save will not load, openttd.cpp logs Loading requested map failed; closing server. and exits, and Restart=always retries the same file every 10 seconds. Spot it with journalctl -u openttd | grep 'Loading requested map failed', then sudo systemctl stop openttd, move that file out of the autosave folder (into /srv/openttd/save/, say) and start the service again.
Backups, restore and version upgrades
Stopping the service makes the exit save, so archive with it stopped. This takes the whole data folder (saves, all three config files, the base set and downloaded content) plus the start script and unit, checks the archive and starts the server again either way:
sudo install -d -m 700 /root/openttd-backups
sudo systemctl stop openttd
f=/root/openttd-backups/openttd-$(date +%F-%H%M%S).tar.gz
sudo tar -C / -czf "$f" srv/openttd usr/local/bin/openttd-start.sh etc/systemd/system/openttd.service && sudo tar -tzf "$f" >/dev/null && echo "archive ok: $f"
sudo systemctl start openttd
Copy archives off the machine to a host you control with plain rsync -a, never --delete. To restore, as root or with sudo: sudo systemctl stop openttd; check the file with sudo tar -tzf FILE >/dev/null && echo ok and treat anything but ok as a stop; then [ ! -e /srv/openttd.previous ] && sudo mv /srv/openttd /srv/openttd.previous && sudo tar -C / -xzf FILE srv/openttd && sudo chown -R openttd:openttd /srv/openttd && echo restored, which sets the current saves and config aside together and refuses while an older .previous is still there; start the service only on restored. To undo it: sudo systemctl stop openttd && [ -d /srv/openttd.previous ] && [ ! -e /srv/openttd.undone ] && sudo mv /srv/openttd /srv/openttd.undone && sudo mv /srv/openttd.previous /srv/openttd && sudo systemctl start openttd. Once the world looks right, sudo rm -rf /srv/openttd.previous so a later restore is not refused (after an undo, remove /srv/openttd.undone the same way).
Clients must match the server's version exactly: docs/multiplayer.md says a server shown with a red dot runs a different version and cannot be joined. To upgrade, back up first, unpack the new archive into /opt/openttd-new after checking its SHA-256, stop the service, run [ ! -e /opt/openttd.old ] && sudo mv /opt/openttd /opt/openttd.old && sudo mv /opt/openttd-new /opt/openttd, and start it. The README guarantees every revision loads saves from older ones; it promises nothing the other way, so roll back by restoring the pre-upgrade archive as well as the old binary.
Which of our machines fit
The game server machines are whole single-tenant servers, and the processor table there lists single-socket chips rated 2,000 or more on PassMark's single-thread test, a bar that page takes from the Satisfactory dedicated server documentation. OpenTTD sets no such bar; our view is that a small map with a few companies runs on the entry machine sold in all five cities, and a map heavy with trains pays for a higher row. The official archive is x86_64 only, so leave out the ARM lines (X-Gene and Ampere Altra). Every server includes one IPv4 address on an unmetered port, and IPv6 can be added free at checkout so players can reach it both ways.
New York, Miami, San Francisco, Amsterdam and Bucharest all sit on our own network, AS19624; the data center page lists one test IP per city, plus a speed test, for players to measure from home before you order. Ubuntu is one of the images on the Linux server page and the root login is yours. A racked machine is live about 30 minutes after the order is approved; a build to order takes 4 to 24 hours. Ready units can also be rented by the hour, enough to try a map and a player count before a month. Rack servers and blades have an out-of-band controller (HPE iLO, Dell iDRAC or a Supermicro BMC) whose browser console the dashboard publishes once you add a console login; among Moonshot cartridges only the m510 and m710x have a console, the rest have power control only.
Wrong fit: installing, updating and moderating the game is yours; the machine comes without a game panel. OpenTTD is GPL v2. In our general reading, not legal advice, running it for players owes nothing, but handing out copies, modified or not (a patched client your players need, say), means passing on the source too.
Questions
Which port do I forward or open?
3979, over both TCP and UDP, as network/core/config.h defines it and the wiki's dedicated server page says to forward. The admin port 3977 only listens once you set an admin password or authorized keys, and this setup leaves it closed in ufw. The coordinator, STUN and TURN ports are outbound only.
How do I keep the game from running while nobody plays?
Set min_active_clients = 1 in the [network] section of openttd.cfg with the server stopped. network.cpp pauses the game while fewer active players than that are in a company, and spectators are not counted. pause_on_join, on by default, also pauses while a joining player loads the map.
Can one machine host two OpenTTD servers?
Yes, as two copies of everything: a second data folder with its own three config files, its own server_port (and admin port if you use one), its own start script, unit and firewall rule.