Build and Host a Multiplayer Web Game. Completely FREE. (Part III)

We will build and upload a game server in the final part of this article series. The clients we implemented in previous posts will connect to this server, and we will have a simple but functional web multiplayer game. We will only use free services.

Building Server Infrastructure with Terraform

First, we will extend our Terraform script (terraform.yml), by adding a virtual machine, placing it in a virtual network, a subnet, and assigning a public IP to it:

resource "azurerm_virtual_network" "this" { name = "mygame-vnet" address_space = [""] location = azurerm_resource_group.this.location resource_group_name = } resource "azurerm_subnet" "this" { name = "internal" resource_group_name = virtual_network_name = address_prefixes = [""] } resource "azurerm_public_ip" "this" { name = "mygame-pip" resource_group_name = location = azurerm_resource_group.this.location allocation_method = "Dynamic" } resource "azurerm_network_interface" "this" { name = "mygame-nic" location = azurerm_resource_group.this.location resource_group_name = ip_configuration { name = "mygame-publicip" subnet_id = public_ip_address_id = private_ip_address_allocation = "Dynamic" domain_name_label = "mygame" } } resource "azurerm_linux_virtual_machine" "this" { name = "mygame-vm" resource_group_name = location = azurerm_resource_group.this.location size = "Standard_B1s" admin_username = "mygamevmuser" admin_password = "GROI43gdfVCB+!1" disable_password_authentication = false network_interface_ids = [, ] os_disk { caching = "ReadWrite" storage_account_type = "Standard_LRS" } source_image_reference { publisher = "Canonical" offer = "UbuntuServer" sku = "16.04-LTS" version = "latest" } }
Code language: JavaScript (javascript)

Now commit your new code, push it to GitHub, and execute the Terraform workflow.

Build Your Game Server

Because our game code is already in the GitHub repository, it is extremely simple to build a game server. We simply need to create a new workflow (let’s call it BuildServer.yml) that builds our game with a custom parameter (EnableHeadlessMode). Otherwise, the steps are the same as it was at the game client:

name: Build Linux Server on: workflow_dispatch: {} jobs: buildServerForLinux: name: Linux Build runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 with: lfs: true - name: Cache uses: actions/cache@v2 with: path: Library key: Library-Linux - name: Build uses: game-ci/unity-builder@v2 with: customParameters: -EnableHeadlessMode targetPlatform: StandaloneLinux64 env: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} - name: Upload uses: actions/upload-artifact@v2 with: name: mygameserver path: build/StandaloneLinux64
Code language: JavaScript (javascript)

Deploy Your Game Server

We currently have a virtual machine in Azure and a game server built in GitHub. Our last task is to transfer this game server to the virtual machine. You can also place the deploying steps immediately after the build job. Then you have an end-to-end workflow. Let us now create a separate deploying workflow. Make a new file (./github/workflows/DeployServer.yml) and paste in the following code:

name: Deploy Linux Server on: workflow_dispatch: {} jobs: deployClientToAppService: name: Deploy Server to Azure VM runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Download artifact uses: dawidd6/action-download-artifact@v2 with: workflow: BuildServer.yml workflow_conclusion: success name: mygameserver path: build/StandaloneLinux64 - name: copy file uses: canastro/copy-file-action@master with: source: "Service/mygameserver.service" target: "build/" - name: copy file via ssh password uses: appleboy/scp-action@master with: host: ${{ secrets.AZURE_SERVER_HOST }} username: ${{ secrets.AZURE_SERVER_USERNAME }} password: ${{ secrets.AZURE_SERVER_PASSWORD }} source: "build/" target: "/home/mygamevmuser/mygameserver" - name: executing remote ssh commands using password uses: appleboy/ssh-action@master with: host: ${{ secrets.AZURE_SERVER_HOST }} username: ${{ secrets.AZURE_SERVER_USERNAME }} password: ${{ secrets.AZURE_SERVER_PASSWORD }} script: | chmod 755 /home/mygamevmuser/mygameserver/build/StandaloneLinux64/StandaloneLinux64 ls -l /home/mygamevmuser/mygameserver/build/ sudo mv /home/mygamevmuser/mygameserver/build/mygameserver.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable mygameservice sudo systemctl start mygameservice sudo systemctl status mygameservice - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: restart VM run: az vm restart -g mygame-resources -n mygame-vm --no-wait
Code language: JavaScript (javascript)

To download the built artifact from the build workflow, we will make use of a special GitHub Action (dawidd6/action-download-artifact@v2). If the build and deploying are in the same workflow, we could use the standard GitHub upload/download method.

The virtual machine should start our game server build at every startup. We can achieve that by creating a service (Service/mygame.service) with the following content:

[Unit] Description=MyGame Server [Service] Type=simple ExecStart=/home/mygamevmuser/mygameserver/build/StandaloneLinux64/StandaloneLinux64 [Install]
Code language: JavaScript (javascript)

Commit and push this file to our repository, and copy this file by our deployment workflow with SCP to the virtual machine. For that, set some GitHub secrets. One of them is the AZURE_SERVER_HOST, which is your public IP:

az vm show -d -g mygame-resources -n mygame-vm --query publicIps -o tsv

The AZURE_SERVER_USERNAME and AZURE_SERVER_PASSWORD you can copy directly from the Terraform script.

You can copy the AZURE_SERVER_USERNAME and AZURE_SERVER_PASSWORD directly from the Terraform script, then we only need to start and enable this game server service, for it to run any time the virtual machine starts up.

To restart the virtual machine, we have to use Azure CLI. To achieve that, let’s set the AZURE_CREDENTIALS secret. We need to create a service principal and grant at least a contributor role on our subscription:

az account show --query=id az ad sp create-for-rbac --name "github-action" --role contributor --scopes /subscriptions/<subscription_id> --sdk-auth { "clientId": "<clientId>", "clientSecret": "<clientSecret>", "subscriptionId": "<subscriptionId>", "tenantId": "<tenantId>", "activeDirectoryEndpointUrl": "", "resourceManagerEndpointUrl": "", "activeDirectoryGraphResourceId": "", "sqlManagementEndpointUrl": "", "galleryEndpointUrl": "", "managementEndpointUrl": "" }
Code language: PHP (php)

Copy the result of the above command to the AZURE_CREDENTIALS secret in GitHub.

Commit and push your new workflow to the GitHub repository and run the workflow manually. The server is ready to accept client connections.

Putting it All Together

Start again your application from your browser:

az webapp config hostname list --webapp-name mygame-app-service -g mygame-resources --query=[0].name ""
Code language: PHP (php)

If you try to start it from another browser window, you should be able to join the game as another player. The movement of each player is also visible to all other players.


In this article, we created a virtual machine in Azure to host our game server. We built and deployed the game server with the help of GitHub Actions. This simple setup shows an end-to-end solution for a multiplayer web game. Throughout the setup, we made use of only free services, which suits well for learning the general idea.

I purposefully avoided topics like security (don’t leave your VM’s ssh port open), scalability, performance, and a slew of other non-functional concerns. Your real published game should make use of paid services and incorporate those features. I may address them in a subsequent or later post.

Leave a Comment

Your email address will not be published. Required fields are marked *