Managing Multiple Game Server Instances with SteamCMD and systemd on One Dedicated Machine

A single dedicated machine with 16 fast cores, 128 GB of RAM, and two NVMe Gen4 drives can host eight to twelve game server instances at once, and the per-slot cost drops by a factor of three to five compared with renting an individual server per game. The catch is operational: without disciplined process management, one leaking instance takes down the whole box. SteamCMD plus systemd template units solves that with per-instance users, cgroup memory limits, graceful shutdown signals, and scheduled updates.

Sizing the machine around the instances

Start from the instance list, not the CPU. Each game has a different RAM floor and a different relationship with clock speed:

GameSteam App IDRAM / instanceNet up (32 players)Tick sensitivity
Valheim8966603–4 GB1.5 MbpsHigh (single-thread)
Palworld239401012–16 GB3 MbpsHigh
Project Zomboid3808702–3 GB1 MbpsMedium
7 Days to Die2944206–8 GB2 MbpsHigh
ARK: Survival Ascended243093016–24 GB4 MbpsVery high
Rust2585508–16 GB5 MbpsVery high
Counter-Strike 27304 GB8 Mbps (128-tick)Very high
Terraria (tModLoader)1056001–2 GB0.5 MbpsLow

A realistic 128 GB build: four ARK: Survival Ascended maps at 22 GB each (88 GB), two Valheim instances at 4 GB, one Project Zomboid at 3 GB, and 8 GB left for the OS, systemd, and the SteamCMD update spool. That is seven instances on a Ryzen 9 7950X3D with two 2 TB NVMe Gen4 drives — one for worlds and saves, one for the OS and Steam depot cache.

Directory and user layout

# One system user per instance keeps ownership and ports isolated
sudo useradd -r -m -d /srv/gameservers/valheim1 -s /usr/sbin/nologin valheim1
sudo useradd -r -m -d /srv/gameservers/asa1     -s /usr/sbin/nologin asa1

# Layout
/srv/gameservers/<instance>/        # install dir (SteamCMD force_install_dir)
/srv/gameservers/<instance>/data/   # worlds, saves, configs
/etc/gameservers/<instance>.env     # per-instance variables

One systemd template unit for every instance

A single template file covers all instances. The %i placeholder expands to the instance name, which is also the Linux user and the environment file basename.

# /etc/systemd/system/[email protected]
[Unit]
Description=Game server instance %i
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=%i
Group=%i
EnvironmentFile=/etc/gameservers/%i.env
WorkingDirectory=/srv/gameservers/%i
ExecStartPre=/usr/games/steamcmd +force_install_dir /srv/gameservers/%i \
  +login anonymous +app_update ${STEAM_APPID} validate +quit
ExecStart=/srv/gameservers/%i/${START_CMD}
Restart=on-failure
RestartSec=15
KillSignal=SIGINT
TimeoutStopSec=180
LimitNOFILE=100000
MemoryMax=${MEM_MAX}
CPUQuota=${CPU_QUOTA}
CPUWeight=200
Nice=-5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
# /etc/gameservers/asa1.env
STEAM_APPID=2430930
START_CMD=ShooterGame/Binaries/Linux/ArkAscendedServer TheIsland_WP?listen?Port=7777?QueryPort=27015?MaxPlayers=50 -server -log
MEM_MAX=24G
CPU_QUOTA=600%

# /etc/gameservers/valheim1.env
STEAM_APPID=896660
START_CMD=valheim_server.x86_64 -name "Community" -port 2456 -world Dedicated -public 1 -savedir /srv/gameservers/valheim1/data
MEM_MAX=4G
CPU_QUOTA=200%

Two settings do the heavy lifting. KillSignal=SIGINT tells most game servers to flush their world to disk before exiting — without it, a restart can roll back to the last autosave. And MemoryMax means a Palworld memory leak hits a cgroup wall and restarts one instance instead of triggering the kernel OOM killer and taking down every server on the machine.

Starting, stopping, and watching instances

sudo systemctl daemon-reload
sudo systemctl enable --now gameserver@valheim1 gameserver@pz1 gameserver@asa1
sudo systemctl status 'gameserver@*' --no-pager
sudo journalctl -u gameserver@asa1 -f
sudo systemctl restart gameserver@asa1        # SIGINT, then restart
sudo systemctl stop gameserver@asa1           # graceful save on exit
systemd-cgtop                                  # per-instance CPU and memory

Scheduled updates without killing players

Run SteamCMD updates on a timer rather than on boot only, and use a per-instance lock so two instances never validate the same depot simultaneously. Large updates can rewrite 20–40 GB, so schedule them off-peak and broadcast a countdown over RCON first.

# /etc/systemd/system/[email protected]
[Unit]
Description=SteamCMD update for %i
[Service]
Type=oneshot
User=%i
ExecStart=/usr/bin/flock /run/gameserver-%i.lock /usr/games/steamcmd \
  +force_install_dir /srv/gameservers/%i +login anonymous \
  +app_update ${STEAM_APPID} validate +quit

# /etc/systemd/system/[email protected]
[Unit]
Description=Daily update check for %i
[Timer]
OnCalendar=*-*-* 05:00
Persistent=true
RandomizedDelaySec=30m
[Install]
WantedBy=timers.target

Then systemctl enable --now [email protected] for each instance. RandomizedDelaySec staggers the jobs so eight SteamCMD processes do not saturate the uplink at 05:00.

Port planning

Every instance needs a unique game port and query port. Reserve a block of 16 per instance and document it, because port collisions are the most common reason a second instance fails to appear in the server browser:

InstanceGame portQuery portRCON port
valheim1245624572458
asa177772701527020
pz1162611626216263
cs2-1270152701627017
rust1280152801628017

Disk I/O isolation and backups

Multiple instances share one storage device, which makes I/O isolation the second-biggest failure mode after RAM. Two habits prevent it:

  • Separate world saves from the Steam depot. Updates rewrite tens of gigabytes; if that write runs on the same NVMe as eight world directories, autosaves will stall behind it. Use one Gen4 drive for /srv/gameservers/*/data and a second for /usr/games/steamcmd and depot caches.
  • Throttle backups. An rsync or restic job that reads eight worlds at full disk speed will add hundreds of milliseconds of latency to every instance; run it with ionice -c2 -n7 and nice -n 19, or use cgroup IOWeight per instance.
  • Snapshot per instance. A nightly systemctl stop gameserver@x is too disruptive; instead rely on filesystem snapshots (ZFS or btrfs) so one corrupt world is recoverable in seconds without touching the other seven.
  • Watch inode and disk-space alarms. Five instances writing logs and world backups will fill a 2 TB drive faster than expected; alert at 80%.

Storage layout on a two-drive box: NVMe A holds the OS, SteamCMD, and depot cache; NVMe B holds every instance’s install directory and world data. If a game server must write saves and read its own binary from the same device, keep at least 25% of the drive free so garbage collection on the SSD does not stall writes.

Operational habits that keep the box up

  • Alert on cgroup memory pressure per instance with systemd-cgtop or node_exporter, not just overall RAM.
  • Snapshot the worlds directory nightly; a corrupt save on one instance should never require a full-machine restore.
  • Keep the Steam depot cache on a separate NVMe from the world saves so an update download cannot stall an autosave.
  • Reboot the whole machine monthly, with instances stopped gracefully first, to clear firmware-level issues.

Running many instances on one box works best when the hardware underneath is genuinely dedicated and stable — an oversubscribed host makes per-instance cgroup tuning pointless. If you are still shortlisting machines for this setup, review dedicated server specs for game hosting and prioritise high single-thread clocks, NVMe Gen4, and 128 GB of ECC RAM. Our dedicated server comparison covers which providers offer those configurations with the memory headroom multi-instance hosting demands.

Leave a Reply