Installing a game server with SteamCMD

Published on 16 min read

SteamCMD is the shared foundation of almost every game server on Steam. This guide covers the installation, anonymous login, app_update with validate, running it as a systemd service and the typical error messages.

If you run a game server on your own root server, you almost always end up at the same tool: SteamCMD. Whether it is Valheim, Rust, Counter-Strike 2, Palworld, Enshrouded or ARK, the server files live on Valve's infrastructure and are pulled through the same command line client. Once SteamCMD is set up properly, every additional game is just a different number.

This guide covers exactly the places where shorter tutorials stop: the 32-bit libraries on 64-bit systems, the differences between Debian, Ubuntu and the Red Hat family, the error messages word for word, an update procedure that does not restart the server on every cron run, and operation as a systemd service.

What SteamCMD is and what it is not

SteamCMD is the Steam Console Client, a stripped down command line version of the Steam client without a graphical interface. It does exactly one thing well: download applications from the Steam network, update them and verify their files. It does not start a server, it configures nothing and it knows nothing about game logic. After the download the server files sit in a directory, and from that point on it is up to the game itself.

One detail explains the entire library topic: the SteamCMD bootstrapper is still a 32-bit program today. On a pure 64-bit system it is missing the matching runtime libraries, and that is exactly where the first launch fails for most people. The downloaded server files themselves are almost always 64-bit with modern games.

Preparation: a dedicated user instead of root

Game server processes never run as root. They accept connections from the internet, many titles pull mods from the Steam Workshop at runtime, and they execute third party code. A dedicated, unprivileged user costs three commands and limits the damage.

useradd -m -d /home/steam -s /bin/bash steam
mkdir -p /home/steam/steamcmd
chown -R steam:steam /home/steam/steamcmd

SteamCMD explicitly warns about this when it is started as root. Ignore the warning and the problem comes back later: as soon as a directory is owned by root, a later run as steam fails with a write error, and the message gives no hint about the actual cause.

All the commands in this article are entered as root and switch into the service user with runuser -u steam --. On systems with sudo, sudo -u steam works the same way. If you want to take care of basic hardening first, the groundwork is covered in Setting up a new root server and Securing SSH.

The 32-bit libraries, different on every system

This is where the distributions part ways, and the blanket command lists you find online are wrong here on a regular basis.

Debian 12, Debian 13, Ubuntu 22.04 and Ubuntu 24.04

On all four systems the package lib32gcc-s1 from the main archive is enough. It is an amd64 package that brings the 32-bit runtime with it, and it needs no additional i386 architecture:

apt update
apt install -y ca-certificates curl tar file lib32gcc-s1 lib32stdc++6

lib32stdc++6 is not always required for SteamCMD itself, but quite a few older server binaries (everything based on the Source engine, HLDS titles, some Unity servers) do ask for it. Installing it right away saves a troubleshooting session later. file is not part of any of the minimal images and is needed further down for troubleshooting, which is why it is on the list from the start.

AlmaLinux 9, Rocky Linux 9, RHEL 9 and Oracle Linux 9

There the packages have different names and carry the suffix .i686:

dnf -y update
dnf install -y --allowerasing glibc.i686 libstdc++.i686 tar file curl

Both additions are necessary on a fresh minimal installation, otherwise the command aborts. The reason for --allowerasing: EL9 ships curl-minimal, and the full curl collides with it. Without the flag, the call ends on AlmaLinux 9 and Rocky Linux 9 with package curl-minimal ... conflicts with curl provided by curl ... conflicting requests. Dropping curl from the list altogether also gets you there, because curl-minimal already provides /usr/bin/curl. The reason for the leading dnf -y update: if the system is older than the repositories, the fresh i686 package collides with the installed 64-bit package, measured on Rocky Linux 9 as file /usr/share/gcc-11/python/libstdcxx/v6/printers.py from install of libstdc++-11.5.0-14.el9.i686 conflicts with file from package libstdc++-11.4.1-2.1.el9.x86_64. On Oracle Linux 9 neither of the two conflicts appears, because the full curl is already installed there.

EPEL is not needed, the packages are in the standard repositories. If you want additional tools such as htop, the route is described in Installing htop on AlmaLinux, Rocky and RHEL.

AlmaLinux 10, Rocky Linux 10 and RHEL 10 are unsuitable for SteamCMD

This is not a configuration problem, it is a dead end: RHEL 10 and the distributions derived from it have dropped the 32-bit x86 architecture completely. No repository there carries i686 packages, not even with --enablerepo=*, and the install command ends with No match for argument: glibc.i686. Since steamcmd.sh always calls the 32-bit bootstrapper linux32/steamcmd, there is no way around it. For a game server on the Red Hat side, pick the EL9 family instead: AlmaLinux 9, Rocky Linux 9 or Oracle Linux 9.

A second stumbling block on very slim EL installations: if only util-linux-core is installed, runuser is missing, and this guide uses it throughout. In that case run dnf install -y util-linux first. On AlmaLinux 9, Rocky Linux 9, Oracle Linux 9 and on all Debian and Ubuntu systems, runuser is already present.

Why not simply use the steamcmd package?

Ubuntu carries steamcmd in the multiverse component, Debian in non-free, and in both cases the package is built exclusively for the i386 architecture. On a freshly installed 64-bit server you therefore reliably run into:

E: Unable to locate package steamcmd

For it to become a candidate at all, the component has to be enabled and the foreign architecture activated first. On Ubuntu, multiverse is already active in the default installation, so three lines are enough there:

dpkg --add-architecture i386
apt update
apt-cache policy steamcmd

On Debian the component is missing on top of that. Without it, apt-cache policy steamcmd returns no output at all, so not even an error message. First contrib and non-free have to go into the package sources, since Debian 12 usually in /etc/apt/sources.list.d/debian.sources on the Components: line. Only after that does the query work and report the candidate 0~20180105-5 from bookworm/non-free i386 on Debian 12. The apt update between the architecture switch and the query is mandatory: without that run the i386 package lists do not exist and the candidate stays (none).

The effort rarely pays off. The Debian package is a very old wrapper (version 0~20180105-5 in Debian 13) that only downloads the same bootstrapper you can fetch yourself in two lines. The manual installation is identical on every system and therefore much easier to document.

Installing SteamCMD and reading the first launch

runuser -u steam -- curl -sSLo /home/steam/steamcmd/steamcmd_linux.tar.gz https://media.steampowered.com/client/installer/steamcmd_linux.tar.gz
runuser -u steam -- tar -xzf /home/steam/steamcmd/steamcmd_linux.tar.gz -C /home/steam/steamcmd
runuser -u steam -- /home/steam/steamcmd/steamcmd.sh +quit

The third command is the actual test. On the very first call SteamCMD downloads itself, shows a progress display and exits again. It looks like this:

[  0%] Checking for available update...
[----] Downloading update (0 of 58,393 KB)...
[100%] Download complete.
[----] Extracting package...
[----] Installing update...
[----] Verifying installation...
Steam Console Client (c) Valve Corporation - version 1751...
Loading Steam API...OK

Success criterion: the last line reads Loading Steam API...OK. If Loading Steam API...FAILED appears instead, or the process aborts immediately, the 32-bit libraries from the previous section are missing. The typical wording for that is:

steamcmd.sh: line 41: /home/steam/steamcmd/linux32/steamcmd: No such file or directory

That message is misleading, because the file does exist. What is not found is the 32-bit interpreter the binary needs. You can check with file /home/steam/steamcmd/linux32/steamcmd: the output reads ELF 32-bit LSB shared object, Intel 80386, ... interpreter /lib/ld-linux.so.2 and names exactly the missing interpreter. If file itself is missing with command not found, it was skipped in the installation step above. The variant error while loading shared libraries: libstdc++.so.6 points at the same problem, just one level later.

Anonymous login versus account login

The vast majority of dedicated servers are published as their own free Steam application and can be downloaded without any credentials:

+login anonymous

That is the normal case and the route you should always try first. No password on the server, no Steam Guard, no locked accounts after a server migration.

An account login is only necessary when the server build is tied to owning the game. You can spot it by this message:

ERROR! Failed to install app 'ID' (No subscription)

No subscription always means the same thing: this account is not allowed to download this app. With an anonymous login it means that a real account owning the game is required. With a real account it means the license is missing or the wrong app ID was used.

The account login runs interactively, because Steam Guard asks for a code:

/home/steam/steamcmd/steamcmd.sh +login myusername

After the one-time confirmation a sentry file sits under ~/.steam, and from then on non-interactive calls work too. Three points about this that hurt in practice: use a separate Steam account for this purpose that holds nothing but the server license. Never write the password into a cron script. And expect Steam to ask for another confirmation after a longer period of inactivity or after an IP change, which makes an automated update hang silently.

The Game Server Login Token (GSLT) is a separate matter. It has nothing to do with the download, only with whether the running server appears publicly in the server list. For Counter-Strike 2 and other Valve titles it is generated in the Steam account and entered in the server configuration, not in SteamCMD.

Installing a game: order, validate and branches

SteamCMD works through the + arguments strictly from left to right. That leads to the single most important rule of all:

+force_install_dir has to come before +app_update. If it comes after, the files end up in the default path and the directory you specified stays empty.

A complete call, here with the Valheim server and app ID 896660 as the example:

runuser -u steam -- mkdir -p /home/steam/valheim
runuser -u steam -- /home/steam/steamcmd/steamcmd.sh +force_install_dir /home/steam/valheim +login anonymous +app_update 896660 validate +quit

Note that validate is written without a plus sign: it is an argument of app_update and not a command of its own. validate compares every file against the checksum from the depot and re-downloads anything that differs. That makes sense on the first installation, after an aborted download and with strange crashes. On every routine update it is redundant and expensive, because the whole installation is read. Two side effects are worth knowing: files you changed yourself that belong to the depot are reset, and with games that keep mods inside the installation directory, validate can remove foreign files.

The closing line serves as your success check:

Success! App '896660' fully installed.

A different branch, for example a public test branch, is appended directly to app_update:

+app_update 896660 -beta public-test validate

For a password protected branch, -betapassword comes on top. The way back to the default version is -beta none; simply leaving the parameter out is not enough, because the branch selection stays stored in the manifest.

When it goes wrong: the error messages word for word

SteamCMD reports problems as a hex status that says nothing without a translation. The most common ones:

  • Error! App '...' state is 0x202 after update job: not enough storage. Check with df -h /home/steam. Keep in mind that on top of the target directory SteamCMD needs room for the download cache, usually on the same filesystem under steamapps/downloading. With large titles the peak requirement during an update can be almost twice the final size. If the disk is full, Cleaning up a full disk on Linux helps.
  • Error! App '...' state is 0x606 after update job: write error. In nine out of ten cases it is permissions, not the hardware. The classic: the first run happened as root, the second as steam. Repair it with chown -R steam:steam /home/steam. With symbolic links to a second drive, the target has to belong to the service user as well.
  • Error! App '...' state is 0x402 after update job: no usable connection to the Steam content servers. Usually an outbound firewall rule or a name server problem. SteamCMD needs outbound TCP 443 plus 27015 to 27050.
  • No subscription: a license question, see the previous section.
  • Failed to load steamclient.so or [S_API FAIL] SteamAPI_Init(): the server binary looks for the Steam library in a fixed location that SteamCMD does not populate. Practically every Unreal Engine server is affected, as well as Rust, Palworld and V Rising. The fix is two symbolic links in the home directory of the service user.
runuser -u steam -- mkdir -p /home/steam/.steam/sdk64
runuser -u steam -- ln -sf /home/steam/steamcmd/linux64/steamclient.so /home/steam/.steam/sdk64/steamclient.so
runuser -u steam -- mkdir -p /home/steam/.steam/sdk32
runuser -u steam -- ln -sf /home/steam/steamcmd/linux32/steamclient.so /home/steam/.steam/sdk32/steamclient.so

Because they point at the SteamCMD directory, they stay valid after SteamCMD updates itself. A common follow-up mistake is creating these directories as root, after which the service cannot read them later on.

If a download stays stuck for good, the last lever is deleting steamapps/appmanifest_<ID>.acf in the installation directory. After that SteamCMD considers the app not installed and downloads it completely again.

Automating updates without pointless server restarts

The common advice is to run app_update once a day from cron and restart the service. That costs you an interruption every day, even when there is no update at all. Comparing the build ID is the better approach. The installed one is in the manifest file, the current one comes from app_info_print:

runuser -u steam -- /home/steam/steamcmd/steamcmd.sh +login anonymous +app_info_update 1 +app_info_print 896660 +quit

That turns into a script at /usr/local/sbin/steam-update.sh:

#!/bin/bash
set -euo pipefail
APPID=896660
DIR=/home/steam/valheim
CMD=/home/steam/steamcmd/steamcmd.sh
UNIT=valheim

MANIFEST="$DIR/steamapps/appmanifest_${APPID}.acf"
installed=$(awk '/"buildid"/ {gsub(/"/,"",$2); print $2; exit}' "$MANIFEST" 2>/dev/null || echo 0)
latest=$(runuser -u steam -- "$CMD" +login anonymous +app_info_update 1 +app_info_print "$APPID" +quit \
  | awk '/"public"/{f=1} f && /"buildid"/ {gsub(/"/,"",$2); print $2; exit}')

if [ -z "$latest" ]; then
  echo "Could not determine the current build ID, aborting."
  exit 1
fi

if [ "$installed" = "$latest" ]; then
  echo "Up to date (build $installed), no restart."
  exit 0
fi

echo "Updating from $installed to $latest"
systemctl stop "$UNIT"
runuser -u steam -- "$CMD" +force_install_dir "$DIR" +login anonymous +app_update "$APPID" +quit
systemctl start "$UNIT"

The script only restarts when something has really changed, and it aborts cleanly when Steam is not answering at that moment. Without that check an empty query result would be read as an update and the server would be stopped for no reason. Run it from cron, or better from a systemd timer; the basics are covered in Setting up a cron job on Linux.

Keep in mind that many games also require updated clients after a server update. An automatic update in the middle of prime time throws every player off the server. Schedule the run for the early morning hours.

Running it as a systemd service

A game server in a screen session does not survive a reboot. A unit at /etc/systemd/system/valheim.service solves that:

[Unit]
Description=Valheim Dedicated Server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=steam
Group=steam
WorkingDirectory=/home/steam/valheim
Environment=LD_LIBRARY_PATH=/home/steam/valheim/linux64
Environment=SteamAppId=892970
ExecStart=/home/steam/valheim/valheim_server.x86_64 -nographics -batchmode -name "KernelHost" -port 2456 -world "Dedicated" -password "changeMe"
Restart=on-failure
RestartSec=10
KillSignal=SIGINT
TimeoutStopSec=90
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now valheim
systemctl status valheim

Two lines in there matter more than they look. KillSignal=SIGINT makes sure the server saves when it is stopped, because quite a few game servers ignore SIGTERM and lose the last few minutes of progress when they are killed hard. TimeoutStopSec gives it the time it needs for that. A detailed explanation of every field is available in Creating a systemd service, and the approach for a Java server in Starting a Minecraft server automatically.

If the server dies right at startup because of missing RAM, a look at Setting up swap helps. The ports then belong in the firewall, see Setting up UFW, keeping in mind that game servers almost always need UDP and that a TCP-only entry is typically the reason why the server runs but nobody can connect.

App IDs of the common games

The ID is the only thing that changes between games. Titles that can be downloaded anonymously include:

  • 232250 Team Fortress 2
  • 4020 Garry's Mod
  • 222860 Left 4 Dead 2
  • 730 Counter-Strike 2 (client and server have been the same app since the switch from CS:GO, so the download is correspondingly large)
  • 258550 Rust
  • 376030 ARK: Survival Evolved
  • 896660 Valheim
  • 2394010 Palworld
  • 2278520 Enshrouded
  • 1829350 V Rising
  • 294420 7 Days to Die
  • 380870 Project Zomboid
  • 1690800 Satisfactory
  • 581330 Insurgency: Sandstorm
  • 233780 Arma 3
  • 1007 Steamworks SDK Redistributables, tiny and therefore ideal as a functional test of the installation

Do not confuse the server app with the game app. With ARK, for example, 346110 is the game and 376030 is the server. If you are looking for an ID, you will find it in the Dedicated Servers List in the Valve Developer Wiki or on SteamDB. The quick test is always the same: an anonymous app_update, and if No subscription comes back, it was the client ID.

How to tell that it really works

Four checks, in this order:

  1. The files are there. du -sh /home/steam/valheim has to show a plausible size, and steamapps/appmanifest_896660.acf contains a buildid. A directory of a few megabytes means the download was aborted.
  2. The process stays alive for more than a minute. systemctl status valheim shows active (running). A service that restarts every second sits at activating (auto-restart), and journalctl -u valheim -n 50 then gives you the reason.
  3. The port is open, and open as UDP. ss -ulpn | grep 2456 has to name the process. Nothing to see means the server is still initializing or bound to the wrong address.
  4. Reachable from outside. Only then check the firewall and connect from inside the game. Many titles also need a query port; with Valheim that is the server port plus one.

If those four points are in order, the foundation is fine and everything beyond it is game configuration. On a KernelHost root server in the maincubes datacenter in Frankfurt am Main you also get DDoS protection out of our own network, which matters especially for publicly listed game servers, because a public server list makes the IP address visible to everyone. The background is covered in Protecting your server against DDoS attacks.

Frequently asked questions

Why does SteamCMD not start on my 64-bit server?
The SteamCMD bootstrapper is a 32-bit program. Without the matching runtime it reports something along the lines of "linux32/steamcmd: No such file or directory", even though the file does exist. On Debian and Ubuntu this is fixed by "apt install lib32gcc-s1 lib32stdc++6", on AlmaLinux 9, Rocky Linux 9 and Oracle Linux 9 by "dnf -y update" followed by "dnf install --allowerasing glibc.i686 libstdc++.i686". An additional i386 architecture is not needed on Debian and Ubuntu. The EL10 family (AlmaLinux 10 and its relatives) no longer has any i686 packages at all, so SteamCMD cannot be run there.
Do I need a Steam account for a game server?
In most cases no. Dedicated servers are their own free Steam applications and can be downloaded with "+login anonymous". A real account is only necessary when SteamCMD reports "No subscription" during the download. That is a different thing from the Game Server Login Token, which has nothing to do with the download and instead decides whether the running server appears in the public server list.
What does "App state is 0x606 after update job" mean?
That is a write error, and it almost always comes from wrong file permissions rather than a failing disk. The typical cause is a first run as root and a second one as the service user. "chown -R steam:steam /home/steam" cleans that up. The related status 0x202 stands for too little free storage, and there only cleaning up or adding space helps.
Should I pass validate on every update?
No. On the first installation, after an aborted download and with unexplained crashes, validate is the right choice. In routine operation it reads the entire installation on every run and costs unnecessary time and I/O. It also resets files you changed yourself and can remove mods from the installation directory with some games.
How do I update automatically without restarting the server every day?
Compare the build ID. The installed one is in steamapps/appmanifest_<ID>.acf, the current one comes from "+app_info_update 1 +app_info_print <ID>". Only when the two values differ is the service stopped, updated and started again. It is important to abort when the query returns no result, otherwise the script stops the server for no reason during a Steam outage.
Why does apt not find the steamcmd package?
Because it sits in multiverse on Ubuntu and in non-free on Debian, and because it is built exclusively for the i386 architecture. Without the component enabled and without "dpkg --add-architecture i386" you get "E: Unable to locate package steamcmd". Since the package only downloads the same bootstrapper anyway, the manual installation from the tarball is the simpler route and works identically on every distribution.

SteamCMD Game Server Linux Debian Ubuntu systemd Valheim Counter-Strike 2 Root Server