aymengharbi

live in prod, no console errors (yet)

Building bloodstrike backend · Lesson 2 of 4

Deploying a Node.js Game Server on Ubuntu

From a GitHub repository to a persistent systemd service

On this page
  1. Getting the code from GitHub
  2. Installing Node.js and project dependencies
  3. Building and testing the game server
  4. Testing the application locally
  5. Keeping configuration outside the source code
  6. Creating a systemd service
  7. Useful commands for maintenance
  8. Summary

Once the VPS was prepared, the next step was to deploy the Bloodstrike game server. We wanted the application to run reliably without keeping an SSH terminal open. We also wanted it to restart automatically if the process failed.

Getting the code from GitHub

Our source code was stored in a private GitHub repository. The VPS needed a secure way to download it without using a personal account password.

We created a dedicated SSH deploy key on the server and added its public key to the repository as a read-only deploy key.

We then tested the connection to GitHub.

ssh -T git@github.com

After confirming access, we cloned the project.

mkdir -p ~/apps
cd ~/apps
git clone YOUR_PRIVATE_REPOSITORY_SSH_URL cs-bloodstrike
cd cs-bloodstrike

Use the SSH URL of your own repository instead of the placeholder.

A deploy key is useful because it gives the server access to a specific repository without granting access to an entire personal GitHub account.

Installing Node.js and project dependencies

The game server is a Node.js application. We installed a supported Node.js release and checked the versions.

node -v
npm -v

Our environment used Node.js 24 and npm 11.

We installed the dependencies recorded in the lockfile.

cd ~/apps/cs-bloodstrike
npm ci

The npm ci command installs the locked dependencies. It is well suited to repeatable deployments because it follows the lockfile rather than freely changing dependency versions.

Building and testing the game server

Installing dependencies does not automatically mean that the application is ready to run.

We built the server and ran the project’s tests.

npm run build:server
npm run test:server

Both commands completed successfully in our setup.

The build generated the production server bundle at:

apps/game-server/dist/server.mjs

The project uses ws for WebSocket support. The package was present in the game-server workspace.

npm ls ws

This is an important distinction: the game server itself handles WebSocket connections. We do not need to install a second WebSocket server just to enable secure connections.

Testing the application locally

Before making the application available through a public domain, we tested its health endpoint on the VPS.

The server supports a configurable host and port. We used a local-only binding for this quick test.

HOST=127.0.0.1 PORT=8080 npm run start:server

In another terminal session, we ran:

curl -i http://127.0.0.1:8080/health

The response contained:

HTTP/1.1 200 OK

ok

This confirmed that the application started and responded to HTTP requests.

After the test, we stopped the manually started process. The production process would be managed by systemd instead.

Keeping configuration outside the source code

Applications often need configuration values such as a listening port, allowed browser origins, and connection limits.

We stored these settings in a dedicated environment file:

/etc/bloodstrike-game.env

A simplified example looks like this:

HOST=127.0.0.1
PORT=8080
ALLOWED_ORIGINS=https://your-frontend.example
MAX_ROOMS=50
MAX_PLAYERS_PER_ROOM=16
MAX_CONN_PER_IP=8
TRUST_PROXY=0
COUNTDOWN_MS=3000

Replace the example frontend origin with the real origin of your application.

The server listens on 127.0.0.1, not 0.0.0.0. This means the game process accepts connections through the local machine rather than listening directly on every network interface.

Caddy will later receive public traffic and forward it to this local address.

We protected the environment file so that only root could read and modify it.

sudo chown root:root /etc/bloodstrike-game.env
sudo chmod 600 /etc/bloodstrike-game.env

This is especially important if secrets are added in the future.

Creating a systemd service

We wanted the game server to start automatically when the VPS boots and restart if the process fails.

We created a systemd unit at:

/etc/systemd/system/bloodstrike-game.service

Its essential configuration was:

[Unit]
Description=Bloodstrike Game Server
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/apps/cs-bloodstrike/apps/game-server
Environment=NODE_ENV=production
EnvironmentFile=/etc/bloodstrike-game.env
ExecStart=/usr/bin/node /home/ubuntu/apps/cs-bloodstrike/apps/game-server/dist/server.mjs
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

The service runs as ubuntu, not root. It loads its environment from the protected configuration file and starts the production bundle.

The restart settings tell systemd to restart the process if it exits because of a failure.

After creating the file, we loaded and enabled the service.

sudo systemctl daemon-reload
sudo systemctl enable --now bloodstrike-game

We checked its status.

sudo systemctl is-active bloodstrike-game

The expected output was:

active

We then repeated the health check.

curl -i http://127.0.0.1:8080/health

The application returned 200 OK.

Useful commands for maintenance

Once an application runs as a service, these commands make everyday maintenance easier.

Check the service:

sudo systemctl status bloodstrike-game --no-pager

Restart it after changing its environment:

sudo systemctl restart bloodstrike-game

Read recent logs:

sudo journalctl -u bloodstrike-game -n 100 --no-pager

Follow the logs live:

sudo journalctl -u bloodstrike-game -f

These commands help diagnose problems without manually launching the server.

Summary

We cloned the private repository, installed dependencies, built and tested the code, and deployed the production server as a systemd service.

The game server now runs in the background and listens locally on port 8080. It is not directly exposed to the public network.

In the next lesson, we will connect a domain to the VPS and configure HTTPS with Caddy.