Use case · Game server

Space Station 14 server on bare metal: SS14.Watchdog, ports, host rights, backups

A Space Station 14 server is Robust.Server, the headless build of the game, run under SS14.Watchdog, which the SS14 docs recommend for proper deployments. It fetches official builds from a manifest, restarts the game server when it stops checking in, and takes update and restart calls over an HTTP API. Players need UDP 1212 for netcode and TCP 1212 for the status API the launcher reads. This page builds the watchdog on Ubuntu 24.04 with .NET 10 and covers systemd, host rights, the hub, updates, rollback and backups.

See the machines

What the server asks of the machine

The SS14 docs set no minimum specification. Their infrastructure reference does list the hardware behind the official Wizard's Den servers. A Ryzen 5 5600X (6 cores) with 16 GB of RAM and a 256 GB SSD carries two game servers, and elsewhere a virtual guest with 6 dedicated CPUs and 16 GB carries two more. The busiest of those boxes peaks at about 5 TB of traffic a month.

WhatDefaultSet by
Game netcodeUDP 1212net.port
Status API (launcher, hub)TCP 1212status.bind
Watchdog API127.0.0.1:5000 hereUrls, BaseUrl in appsettings.yml
DatabaseSQLite preferences.db[database] engine

net.bindto defaults to 0.0.0.0,:: (CVars.cs), so the netcode answers on IPv6 as well once the machine has an address; IPv6 is not in default builds and is added at checkout. The soft cap defaults to 30 players (game.soft_max_players, CCVars.Game.cs) and the tick rate to the guide's 30 (net.tickrate, CVars.cs), which config.toml leaves alone.

Picking the machine

Every processor on our game server line-up is rated 2,000 or more on PassMark's single-thread test, a bar that page takes from the Satisfactory docs, not SS14's. The Xeon E3-1585L v5 entry build (4 cores, 32 GB DDR4, NVMe) is sold in all five cities; an E-2286M with 8 cores in Bucharest, or an EPYC 72F3 in New York or Bucharest, leaves room for more instances. We have not benchmarked SS14 on them.

  • Take an x86 line, because the commands build for linux-x64, so the ARM lines, the X-Gene cartridge among them, are outside them.
  • Pick the city by ping; the data centers page lists a test IP per city.
  • Ready units rent by the hour with a one-hour minimum, enough for a playtest weekend; a permanent station runs monthly.
  • Ready servers go live about 30 minutes after the order is approved; a build to your configuration takes 4 to 24 hours.
  • The 1 Gbps port on these builds is unmetered.

Block 1, build SS14.Watchdog

Install Ubuntu 24.04 from the Linux images and log in as a sudo user. Microsoft's Ubuntu page shows .NET 10 in the built-in 24.04 feed and says the SDK includes the runtimes, so dotnet-sdk-10.0 covers the watchdog's two prerequisites. The block adds ufw, a no-login ss14 account, the watchdog published into /home/ss14/watchdog, the unit, and a helper, /usr/local/sbin/ss14, for ready, update, restart, revert and versions. It then enables ufw with IPv6 rules, OpenSSH and 1212 UDP and TCP. The OpenSSH profile is port 22 only, so if sshd listens elsewhere, run sudo ufw allow PORT/tcp before block 1.

if sudo apt-get update && sudo apt-get install -y dotnet-sdk-10.0 git sqlite3 curl ufw; then
  getent passwd ss14 >/dev/null || sudo useradd --system --create-home --shell /usr/sbin/nologin ss14
  if sudo test -e /home/ss14/SS14.Watchdog || sudo test -e /home/ss14/watchdog; then echo "STOP - built before"
  else
    sudo -u ss14 -H bash -c 'cd ~ && git clone --recursive https://github.com/space-wizards/SS14.Watchdog && dotnet publish SS14.Watchdog/SS14.Watchdog -c Release -r linux-x64 --no-self-contained -o /home/ss14/watchdog' &&
    sudo tee /etc/systemd/system/ss14-watchdog.service >/dev/null <<'EOF' &&
[Unit]
Wants=network-online.target
After=network-online.target
[Service]
User=ss14
WorkingDirectory=/home/ss14/watchdog
ExecStart=/home/ss14/watchdog/SS14.Watchdog
Restart=on-failure
OOMPolicy=continue
[Install]
WantedBy=multi-user.target
EOF
    sudo tee /usr/local/sbin/ss14 >/dev/null <<'EOF' &&
c=$*; if [ "$c" = ready ]; then for i in $(seq 60); do curl -fso /dev/null 127.0.0.1:1212/info && echo READY && exit; sleep 5; done; echo STOP; exit 1; fi
[ "$c" = versions ] && m=GET || m=POST
curl -fsS -X $m --netrc-file /root/ss14-api.netrc "127.0.0.1:5000/instances/station/$c"; echo
EOF
    sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && sudo ufw allow OpenSSH && sudo ufw --force enable && sudo ufw allow 1212/udp && sudo ufw allow 1212/tcp &&
    sudo systemctl daemon-reload && echo "BUILD OK" || echo "STOP - see above"
  fi
else echo "STOP - apt did not finish"; fi

Our departures from the docs are these. The publish names the project and writes to /home/ss14/watchdog with -o rather than copying the output. The unit is named ss14-watchdog, where the docs use SS14.Watchdog, so use our name with systemctl. It runs as User=ss14 instead of root, waits for network-online.target, installs under multi-user.target, has no Description, and drops DOTNET_CLI_HOME=/tmp, kept by the docs for Git updates. If the block stops after the clone on a fresh machine, run sudo rm -rf /home/ss14/SS14.Watchdog /home/ss14/watchdog and paste it again.

Block 2, instance and first start

Only if block 1 printed BUILD OK, set NAME (the launcher name) and ADMIN (your SS14 account) and run block 2; it refuses while either reads CHANGE_ME. It writes a random API token to /root/ss14-api.netrc at mode 600, keeping it off command lines that ps or shell history would show; writes config.toml with the name, the lobby, auth.mode = 1 (accounts required) and console.login_host_user; then sets appsettings.yml to mode 600 and appends the station instance with Manifest updates and BaseUrl and Urls on 127.0.0.1:5000, so the API never binds a public address.

W=/home/ss14/watchdog; NAME="CHANGE_ME"; ADMIN="CHANGE_ME"
if sudo test -x $W/SS14.Watchdog && [ "$NAME" != CHANGE_ME ] && [ "$ADMIN" != CHANGE_ME ] && ! sudo grep -q '^Servers:' $W/appsettings.yml && T=$(openssl rand -hex 24) && [ -n "$T" ]; then
  sudo install -m 600 /dev/null /root/ss14-api.netrc && echo "machine 127.0.0.1 login station password $T" | sudo tee /root/ss14-api.netrc >/dev/null &&
  sudo -u ss14 mkdir -p $W/instances/station && printf '[game]
hostname = "%s"
lobbyenabled = true
[auth]
mode = 1
[console]
login_host_user = "%s"
' "$NAME" "$ADMIN" | sudo -u ss14 tee $W/instances/station/config.toml >/dev/null &&
  sudo chmod 600 $W/appsettings.yml && sudo tee -a $W/appsettings.yml >/dev/null <<EOF &&
BaseUrl: "http://127.0.0.1:5000/"
Urls: "http://127.0.0.1:5000"
Servers:
  Instances:
    station:
      Name: "Station"
      ApiToken: "$T"
      ApiPort: 1212
      UpdateType: "Manifest"
      Updates:
        ManifestUrl: "https://wizards.cdn.spacestation14.com/fork/wizards/manifest"
      TimeoutSeconds: 60
EOF
  sudo systemctl enable --now ss14-watchdog && echo "CONFIG OK" || echo "STOP - see the first error above"
else echo "STOP - no build, a CHANGE_ME left, or already configured"; fi

The first start downloads the server build before Robust.Server runs (ServerInstance.Actor.cs). sudo bash /usr/local/sbin/ss14 ready polls http://127.0.0.1:1212/info for up to five minutes and prints READY or STOP.

Block 2 refuses a re-run once appsettings.yml has the Servers section. If it stopped after writing it, start the unit with sudo systemctl start ss14-watchdog and read journalctl -u ss14-watchdog. Per the docs, a game server restarting every 30 seconds means it cannot reach the watchdog's BaseUrl.

Host rights, hub listing, database

  • Host. A localhost connection gets host automatically, which does not help on a remote box; console.login_host_user grants host to that account when it joins. With auth.mode = 1 every client authenticates, so in our reading no one else can claim that name. The docs warn that +HOST hands over the server and the computer, so keep it and grant admins narrower flags with the in-game permissions command.
  • Hub. Listing is off by default, and turning it on binds you to the hub rules. Put [hub], advertise = true and tags from the standard set in the instance's config.toml and apply it with sudo bash /usr/local/sbin/ss14 restart, which is immediate and drops players (ServerInstance.Actor.cs). "Unable to contact status address" in the log means TCP 1212 is unreachable from outside.
  • Tuning. The hosting guide's tweaks are YAML entries under EnvironmentVariables in the station's entry of appsettings.yml (edit it with sudo), with quoted string values, setting DOTNET_TieredPGO to "1", DOTNET_TC_QuickJitForLoops to "1", DOTNET_ReadyToRun to "0", and ROBUST_NUMERICS_AVX to "true", which can hurt on some processors. They apply after sudo systemctl restart ss14-watchdog, which drops players, so restart on an empty server.
  • Database. SQLite is the default and, per the docs, sufficient for small servers. PostgreSQL mainly matters if several game servers share a database or the server grows large; MySQL and MariaDB are unsupported. Migrations run automatically at startup.

Updates and rollback

The watchdog checks for a new build when you call update or when the watchdog service restarts; a crash restart of the game does not update it. Back up first and send sudo bash /usr/local/sbin/ss14 update. If the manifest has something newer, ServerUpdateManager.cs restarts the game at round end or once the last player leaves. The watchdog fetches the build as it starts the server again, so the station is offline until that download and start finish; sudo bash /usr/local/sbin/ss14 ready confirms it is back.

To roll back, call sudo bash /usr/local/sbin/ss14 revert (InstanceController.cs, added September 2026, Manifest instances only); it returns to the previous build at the next quiet restart. 'revert?immediate=true' switches now and 'revert?version=NAME' picks a build listed by versions, each given to the helper as one quoted argument. If the newer build migrated the database, restore the backup too. To update the watchdog program itself, back up, wait for an empty server, then run `if sudo test ! -e /home/ss14/watchdog.new && sudo -u ss14 -H bash -c 'cd ~/SS14.Watchdog && git pull --recurse-submodules && dotnet publish SS14.Watchdog -c Release -r linux-x64 --no-self-contained -o ~/watchdog.new && rm ~/watchdog.new/appsettings.yml'; then sudo systemctl stop ss14-watchdog && sudo -u ss14 cp -r /home/ss14/watchdog.new/. /home/ss14/watchdog/ && sudo systemctl start ss14-watchdog && sudo bash /usr/local/sbin/ss14 ready || echo "STOP - may be stopped, check systemctl status ss14-watchdog"; else echo "STOP - not built, old watchdog untouched"; fi. It builds while the old watchdog runs, deletes the build's default appsettings.yml, and only then stops the service (a running binary cannot be overwritten), copies the build over as ss14, keeping instances and your settings, and starts it. "not built" means nothing changed; "may be stopped" means start the service and, if the copy broke it, restore the backup. The stop takes the game down too unless the docs' PersistServers and KillMode=process are set. Remove the leftover afterwards (sudo rm -rf /home/ss14/watchdog.new`), or the next run refuses.

Backups and restore

Everything lives in /home/ss14/watchdog. The token is in the watchdog's appsettings.yml, the settings in the instance's config.toml, player data in the instance's data/preferences.db. Block 3 writes a backup script that copies the live database with sqlite3 .backup, runs integrity_check on the copy, archives the folder with that copy and without the live bin/logs (accepting tar's exit code 1 for a file changing mid-read), confirms the copy is inside, and only then prunes archives older than 14 days. It also writes a restore script and a 04:17 cron entry that overwrites backup.log each night.

sudo install -d -m 700 /var/backups/ss14 && sudo tee /usr/local/sbin/ss14-backup.sh >/dev/null <<'EOF' &&
set -o pipefail; umask 077; H=/home/ss14; B=/var/backups/ss14; S=$(date +%F-%H%M%S); P=prefs-$S.db
sqlite3 $H/watchdog/instances/station/data/preferences.db ".backup '$B/$P'" &&
[ "$(sqlite3 $B/$P 'PRAGMA integrity_check;')" = ok ] &&
{ tar -czf $B/ss14-$S.tar.gz --warning=no-file-changed --exclude='*/data/preferences.db*' --exclude='*/bin/logs' -C $H watchdog -C $B $P; [ $? -le 1 ]; } &&
tar -tzf $B/ss14-$S.tar.gz | grep -cx $P >/dev/null && rm -f $B/$P &&
find $B -name 'ss14-*.tar.gz' -mtime +14 -delete && echo "backup OK $S" || { echo "backup FAILED, see above"; exit 1; }
EOF
sudo tee /usr/local/sbin/ss14-restore.sh >/dev/null <<'EOF' &&
A=$*; H=/home/ss14; T=$(mktemp -d -p $H) || exit 1
[ ! -e $H/watchdog.previous ] && tar -tzf "$A" | grep -q '^watchdog/appsettings.yml$' && tar -xzf "$A" -C $T && ls $T/prefs-*.db >/dev/null &&
echo "ARCHIVE OK" && systemctl stop ss14-watchdog && mv $H/watchdog $H/watchdog.previous &&
mv $T/watchdog $H/ && mv $T/prefs-*.db $H/watchdog/instances/station/data/preferences.db && chown -R ss14: $H/watchdog &&
systemctl start ss14-watchdog && bash /usr/local/sbin/ss14 ready || echo "STOP - roll back if ARCHIVE OK showed"
rm -rf $T
EOF
echo '17 4 * * * root umask 077; /bin/bash /usr/local/sbin/ss14-backup.sh > /var/backups/ss14/backup.log 2>&1' | sudo tee /etc/cron.d/ss14-backup >/dev/null && echo "SCRIPTS OK"
  1. Only if block 3 printed SCRIPTS OK, test it with sudo bash /usr/local/sbin/ss14-backup.sh, which must end with backup OK. Copy archives off the machine yourself.
  2. Restore with sudo bash /usr/local/sbin/ss14-restore.sh /var/backups/ss14/ss14-STAMP.tar.gz. It refuses if watchdog.previous exists, extracts and checks the archive before stopping anything (ARCHIVE OK), moves the live folder to watchdog.previous, puts the archive in place with the database copy as preferences.db (no stale -wal or -shm comes along), fixes ownership, starts and waits for READY.
  3. If ARCHIVE OK showed and READY did not, or the data is wrong, this rolls back. `sudo bash -c 'H=/home/ss14; [ -e $H/watchdog.previous ] && [ ! -e $H/watchdog.failed ] && systemctl stop ss14-watchdog && { [ ! -e $H/watchdog ] || mv $H/watchdog $H/watchdog.failed; } && mv $H/watchdog.previous $H/watchdog && systemctl start ss14-watchdog && echo "ROLLED BACK" || echo "STOP - no watchdog.previous, or watchdog.failed exists"'`
  4. When the restore is good, delete watchdog.previous (and any watchdog.failed), or the next run refuses.

On a new machine, run block 1 (it brings the same ufw rules), then block 3, then the restore. After READY, enable the unit and rebuild the mode-600 token file from the restored appsettings.yml with `sudo systemctl enable ss14-watchdog && sudo install -m 600 /dev/null /root/ss14-api.netrc && sudo sed -n 's/^ *ApiToken. "\(.*\)"$/machine 127.0.0.1 login station password \1/p' /home/ss14/watchdog/appsettings.yml | sudo tee /root/ss14-api.netrc >/dev/null && sudo grep -q password /root/ss14-api.netrc && echo "NETRC OK"`.

Licence and limits

Our general reading, not legal advice. The game's content code and SS14.Watchdog are MIT, so copies passed on keep the copyright notice and licence text, and no source is owed. Most assets are CC-BY-SA 3.0 (attribution, and the same licence on adapted assets you distribute); the README says some are non-commercial, such as CC-BY-NC-SA 3.0, and must go for commercial use. The guide allows changes to a packaged build only through its config file, so custom content means a fork and a build of your own.

Questions

Which ports does an SS14 server need open?

UDP 1212 for the game and TCP 1212 for the status API the launcher and hub query. Keep the watchdog API on 127.0.0.1.

Is Robust.Server on its own enough?

For a test, yes. The hosting guide notes a bare server neither restarts after a crash nor updates; the watchdog adds both.

Can one machine host two stations?

Yes. Add a second instance with its own ApiPort, set the same number as net.port in its config.toml (the docs say the watchdog does not sync them), and open that port for UDP and TCP.

Run your station on a machine of its own

x86 hardware with root, an unmetered port, and five cities on AS19624.

See the machines

Related on Server Room

Other use cases