Deploying a Steam dedicated server on Kubernetes

Larbi Youcef Mohamed Reda
alter way
Published in
7 min readJun 25, 2020

Using steamcmd

Today I am going to show you how to deploy a Steam dedicated server on Kubernetes. We will deploy a Counter-Strike: Global Offensive dedicated server as an example, but this method works with a lot of steam based games.

Prerequisites

To install a dedicated server for a Steam game, You need:

  • A Steam account
  • To own the game you want to install a server for (in our case CS:GO)
  • A Steam token (You can create one here)
  • Docker installed
  • A Kubernetes cluster (I am using an AKS cluster)

What is steamcmd ?

To install our dedicated server, we need a tool called “steamcmd”. This tool, released by Valve, is a simple and generic way to install dedicated servers for steam based games.

For instance, we can deploy a CS:GO as follows :

$ ./steamcmd.sh +force_install_dir $HOME/csgo +login anonymous +app_update 740 +quit

This command installs the CS:GO dedicated server in the $HOME/csgo directory. You can find more elements about steamcmd in the documentation right here.

After that, we can launch the server with the following command :

$ $HOME/csgo/srcds_run -game csgo -console -usercon +game_type 1 +game_mode 2 +mapgroup mg_allclassic +map de_dust +sv_setsteamaccount SRCDS_TOKEN -net_port_try 1

We need to replace SRCDS_TOKEN by our Steam token. The different options depends on the game you to install a server for (the documentation can be found here for a CS:GO server).

This command results in a running CS:GO dedicated server. This is pretty cool, but our goal is to deploy the server on Kubernetes. To do so, we are going to :

  • Create a Docker image that contains the CS:GO dedicated server (using steamcmd).
  • Deploy our dedicated server on Kubernetes, using our Docker image.

Creating a the Docker image

To deploy the container hosting our dedicated server on Kubernetes, we need a Docker image. This image will contain an out-of-the-box CS:GO dedicated server. We will make our own Dockerfile.

So basically, to install our dedicated server in our Docker image, we need to :

  • Create a “steam” user (or whatever the name, we just need a user other than root).
  • Install steamcmd.
  • Use steamcmd to deploy our CS:GO dedicated server.
  • Create an entrypoint that launches our server on the container start.

Here is our Dockerfile :

FROM debian:busterARG DEBIAN_FRONTEND=noninteractiveRUN useradd -m steam \
&& apt-get update \
&& apt-get install wget -y \
&& dpkg --add-architecture i386 \
&& apt-get update \
&& apt-get install lib32gcc1 -y
USER steamWORKDIR /home/steamADD --chown=steam:steam https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz /home/steam/COPY --chown=steam:steam csgo_install.txt /home/steam/RUN wget https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \
&& tar xvf steamcmd_linux.tar.gz
RUN /bin/bash /home/steam/steamcmd.sh +runscript /home/steam/csgo_install.txtCOPY --chown=steam:steam entry.sh /home/steam/EXPOSE 27015/udpCMD ["/home/steam/entry.sh"]

As you can see, our steamcmd uses a script to install the dedicated server called csgo_install.txt :

//update CSGO server
login anonymous
force_install_dir /home/steam/csgo
app_update 740 validate
exit

Finally, here is our entrypoint :

#!/bin/bashif [[ -z "$SRCDS_TOKEN" ]]
then
echo "You must give a SRCDS_TOKEN"
exit 1
fi
/home/steam/csgo/srcds_run -game csgo -console -usercon +game_type 1 +game_mode 2 +mapgroup mg_allclassic +map de_dust +sv_setsteamaccount $SRCDS_TOKEN -net_port_try 1

This entrypoint will be executed at the start of our container. It basically checks if we provided the steam token, and launches the CS:GO server.

We can now build our Docker image with the following command :

$ docker build -t csgo:latest .

Note: Since CS:GO is quite big (25 GB), this build might take a while.

Now that we have built our CS:GO Docker image, we can run it on any machine running Docker as follows :

$ docker run -it -p 27015:27015/udp -e SRCDS_TOKEN=XXXXXXXXXXXXXXXXXX csgo:latest

Note: The CS:GO server listens on the 27015 UDP port.

We need to store it in a registry. I am going to use the Azure Container Registry for that purpose. We need to tag our image with the correct name and push it into the registry , as follows :

$ az acr login --name srcds.azurecr.io
$ docker tag csgo:latest csgo:latest srcds.azurecr.io/csgo:latest
$ docker push srcds.azurecr.io/csgo:latest

Our image is now stored in our registry which means that our Kubernetes cluster can now pull it and use it.

Setup the Kubernetes deployment

Now that we have a Docker image, we can deploy our server on Kubernetes.

First of all, we create a namespace called ‘csgo-server’ :

apiVersion: v1
kind: Namespace
metadata:
name: csgo-server

We will create everything needed to setup our CS:GO dedicated server in this namespace.

First of all, we need to store our Steam token on Kubernetes. To do that, we will create a Secret :

apiVersion: v1
data:
SRCDS_TOKEN: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (base64 encoded)
kind: Secret
metadata:
name: steam-token
namespace: csgo-server

We can also customize our dedicated server (Game mode, number of players, etc). This depends on the game you are deploying a dedicated server for. In CS:GO’s case, this is done with files called “server.cfg”, “autoexec.cfg”.

Let’s go ahead and create a ConfigMap from these files :

apiVersion: v1
data:
autoexec.cfg: |
hostname "Counter-Strike: Global Offensive Dedicated Server"
sv_cheats 0 //This should always be set, so you know it's not on
sv_lan 0 //This should always be set, so you know it's not on
exec banned_user.cfg
exec banned_ip.cfg
server.cfg: |
mp_autoteambalance 1
mp_limitteams 1
writeid
writeip
kind: ConfigMap
metadata:
name: csgo-configuration
namespace: csgo-server

Now that we have our Secret to store our Steam token and the configuration for our server, we can now deploy it. For that, we will create a Deployment that creates a container using the Docker image we made previously. We also need to use the Secret and the ConfigMap we have just created. Here is the YAML file for that Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: csgo-deployment
name: csgo-deployment
namespace: csgo-server
spec:
replicas: 1
selector:
matchLabels:
app: csgo-deployment
template:
metadata:
labels:
app: csgo-deployment
spec:
containers:
- image: srcds.azurecr.io/csgo:latest
name: csgo-server
ports:
- containerPort: 27015
protocol: UDP
env:
- name: SRCDS_TOKEN
valueFrom:
secretKeyRef:
name: steam-token
key: SRCDS_TOKEN
volumeMounts:
- name: csgo-configuration-server-cfg
mountPath: /home/steam/csgo/csgo/server.cfg
subPath: server.cfg
- name: csgo-configuration-autoexec-cfg
mountPath: /home/steam/csgo/csgo/autoexec.cfg
subPath: autoexec.cfg

volumes:
- name: csgo-configuration-server-cfg
configMap:
name: csgo-configuration
items:
- key: server.cfg
path: server.cfg
- name: csgo-configuration-autoexec-cfg
configMap:
name: csgo-configuration
items:
- key: autoexec.cfg
path: autoexec.cfg

Last but not least, we need to create a Service to expose our server. We will create a LoadBalancer that targets the 27015 UDP port of our container. We are going to create a static public IP and assign it to our LoadBalancer :

$ az network public-ip create \
--resource-group MC_csgo_csgo-cluster_francecentral \
--name csgo-public-ip \
--allocation-method static \
--sku Standard

And then use it in our LoadBalancer service :

apiVersion: v1
kind: Service
metadata:
labels:
app: csgo-ns
name: csgo-ns
namespace: csgo-server
spec:
ports:
- name: steam
port: 27015
protocol: UDP
targetPort: 27015
selector:
app: csgo-deployment
type: LoadBalancer
loadBalancerIP: 51.103.3.30

Note: The LoadBalancer type is not necessarily supported depending on the cloud provider you are using. In our case, it is supported on AKS.

We also need to configure the outbound IP for our pods to be the same as the LoadBalancer IP. Why are we doing that ? Because when the SRCDS server starts, it calls the Steam servers to register itself (Listening IP based on the outbound IP + Listening port). To be able to connect to our server, we need to use the exact same registered listening IP and listening port (Otherwise, the connection to the server is rejected and we are back to the menu). Which means that our listening IP for our Load Balancer service must be the same as the outbound IP used by our pods. We can do so as follows :

$ az network lb frontend-ip create \
-g MC_csgo_csgo-cluster_francecentral \
-n csgo-frontend \
--lb-name kubernetes \
--public-ip-address csgo-public-ip
$ az network lb outbound-rule update \
--resource-group MC_csgo_csgo-cluster_francecentral \
--lb-name kubernetes \
--name aksOutboundRule \
--frontend-ip-configs csgo-frontend

Alright, we defined all the resources we need to deploy our CS:GO server, we can now deploy it :

$ kubectl apply -f namespace.yaml
$ kubectl apply -f secret.yaml
$ kubectl apply -f configuration.yaml
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml

Note: Since the image is quite big (25 GB), the first creation of our deployment might take a while (the pod is in the ContainerCreating state for a moment, because the image needs to be pulled, which can be a bit long).

Testing our server

Okay then, we can now test our server ! Let’s run the game and use the game’s console to connect to our server:

Connection command

We are now connected. :)

Connected to our CS:GO Server

Of course, our server is pretty basic, but we can easily customize it with our ConfigMap.

Note: You can find all the resources in my personal gitlab repository.

--

--