Files
dev/docs/00 INBOX/ai/steamcmd servers.md

6.3 KiB

SteamCMD servers

This note describes the workflow for adding a server that SteamCMD installs. Start with the generic runner on a host. Then move game behavior into a game module. Move deployment behavior into a guest module after validation.

Stage 1: Run the server with steamcmd

Prove the generic SteamCMD runner before adding game-specific modules. Configure modules.services.steamcmd directly in the host file.

modules.services.steamcmd = {
  enable = true;
  steamDir = "/pikachu/data/steamcmd";
  serviceName = "valheim";
  appId = 896660;
  installDir = "/pikachu/data/servers/valheim/main";
  stateDir = "/pikachu/media/games/servers/valheim/main";
  executable = "./valheim_server.x86_64";
  arguments = [
    "-nographics"
    "-batchmode"
    "-name"
    "main"
    "-port"
    "2456"
    "-world"
    "main"
    "-savedir"
    config.modules.services.steamcmd.stateDir
    "-password"
    "foobarbaz"
    "-public"
    "0"
  ];
  udpPorts = [
    2456
    2457
  ];
};

Use real values for the target game. The example shows Valheim because it was the first server added.

This layer owns shared SteamCMD behavior:

  • service user
  • directory creation
  • SteamCMD install and update command
  • systemd service wrapper
  • firewall ports

Do not pin the game server version in Nix. SteamCMD mutates the installed server files.

Stage 2: Add a game service module

Create a game module after direct SteamCMD deployment works. Use configs/nixos/modules/services/games/<game>.nix.

The game module owns game-specific behavior:

  • Steam app ID
  • server executable
  • command-line flags
  • documented ports
  • default listing policy

The host owns deployment-specific values:

  • paths
  • server name
  • secret choice
  • network placement

A host can run the game module directly during validation:

modules.services.games.valheim = {
  enable = true;
  steamDir = "/pikachu/data/steamcmd";
  installDir = "/pikachu/data/servers/valheim/main";
  stateDir = "/pikachu/media/games/servers/valheim/main";
  serverName = "main";
  passwordFile = "/run/secrets/services/game-servers/password";
};

The game module translates its options into modules.services.steamcmd. It passes the app ID, executable, arguments, and ports to the runner.

Stage 3: Add UDP ingress

Many game servers use UDP. HTTP virtual hosts do not route raw UDP traffic.

Extend registry.route with UDP ports:

registry.route.pikachu.valheim = {
  domain = "valheim.alexion.dev";
  udp.ports = [
    2456
    2457
  ];
};

Keep domain mandatory. Nix does not own public DNS today. The route domain identifies the public route. A future LAN DNS module can also consume it.

The reverse-proxy module joins registry.route with registry.address. It renders nginx stream entries for UDP routes.

server {
  listen 2456 udp;
  proxy_pass 10.23.20.197:2456;
}

Configure public DNS outside Nix:

valheim.alexion.dev -> public IP

Configure LAN DNS outside Nix:

valheim.alexion.dev -> reverse-proxy guest IP

Configure router forwarding outside Nix:

2456/udp -> reverse-proxy guest:2456
2457/udp -> reverse-proxy guest:2457

Use the game documentation to choose the port list. For Valheim, the official guide says it uses the configured port and port plus one.

Stage 4: Move the server into a guest

Add a guest module after host-level validation succeeds. Use configs/nixos/guests/<game>.nix.

The guest module wraps modules.services.games.<game>. It maps host paths into guest paths. It also connects guest address and route options to the registry.

The host config then owns deployment wiring:

guests.valheim = {
  enable = true;
  vlan = 20;
  address = "10.23.20.197";
  steamPath = "/pikachu/data/steamcmd";
  installPath = "/pikachu/data/servers/valheim/main";
  statePath = "/pikachu/media/games/servers/valheim/main";
  serverName = "main";
  reverseProxy = {
    enable = true;
    route = {
      domain = "valheim.alexion.dev";
      udp.ports = [
        config.guests.valheim.port
        (config.guests.valheim.port + 1)
      ];
    };
  };
};

The generic guest machinery publishes registry entries.

guests.valheim.address = "10.23.20.197";

becomes:

registry.address.pikachu.valheim = "10.23.20.197";

The guest route becomes:

registry.route.pikachu.valheim = config.guests.valheim.reverseProxy.route;

Secrets

Use SOPS for game server passwords. Use one shared secret when servers can share a password:

services:
  game-servers:
    password: <password>

The Valheim guest defaults to:

passwordSecret = "services/game-servers/password";

The host decrypts the secret. The guest module bind-mounts the decrypted file into the guest. The game module reads the password file at runtime.

Edit shared secrets with the host key:

cd configs/nixos
sops-host secrets/shared.yaml

Validation

Evaluate the host:

nix eval .#nixosConfigurations.pikachu.config.system.build.toplevel.drvPath --raw

Deploy the host:

deploy-to pikachu

Check the host secret:

sudo test -s /run/secrets/services/game-servers/password && echo secret-ok

Check the guest:

sudo systemctl status container@valheim.service --no-pager

Check the guest secret:

sudo nixos-container run valheim -- test -s /run/secrets/services/game-servers/password

Check the game service:

sudo nixos-container run valheim -- systemctl status valheim.service --no-pager
sudo nixos-container run valheim -- journalctl -u valheim.service -n 200 --no-pager

Wait for the game-specific ready log. For Valheim, wait for these lines:

Game server connected
Opened Steam server

Check game UDP listeners:

sudo nixos-container run valheim -- ss -lunp | grep -E '2456|2457|valheim'

Check reverse-proxy UDP listeners:

sudo nixos-container run reverse-proxy -- ss -lunp | grep -E '2456|2457|nginx'

Test the client through LAN DNS or public DNS:

valheim.alexion.dev:2456

Test direct guest connectivity when ingress fails:

10.23.20.197:2456

Operational notes

A first server start can spend time preparing state. For Valheim, first start can generate a world.

SteamCMD can leave old ownership on migrated server files. The SteamCMD module uses recursive tmpfiles repair for its managed directories.