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

5.9 KiB

SteamCMD servers

This note records the workflow used to add the Valheim server. The workflow starts with the lowest useful layer. Each later step moves game knowledge into a narrower module.

Stage 1: Run one server with steamcmd

Start by proving the generic SteamCMD runner works on a host. 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
  ];
};

This layer owns SteamCMD update behavior and systemd wiring. It also owns directory setup for SteamCMD metadata, server files, and state.

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

Stage 2: Add a game service module

After direct SteamCMD deployment works, create a game module. Use configs/nixos/modules/services/games/<game>.nix.

The game module owns game-specific details:

  • Steam app ID
  • server executable
  • required command-line flags
  • documented UDP ports
  • private or public listing policy

The host keeps deployment-specific values:

  • host paths
  • server name
  • secret choice
  • guest placement, when a guest exists

For Valheim, the host config became:

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";
  password = "foobarbaz";
};

The Valheim module translated that into modules.services.steamcmd. It generated the Valheim argument list and UDP ports.

Stage 3: Add UDP ingress

Valheim uses UDP. HTTP virtual hosts do not route Valheim 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 still 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 ports.

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

Stage 4: Move the server into a guest

After the host-level game module works, add a guest module. Use configs/nixos/guests/<game>.nix.

The guest module wraps modules.services.games.<game>. It also maps host paths into guest paths.

For Valheim, the host config became:

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 the game server password. Use one shared secret for game servers:

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 Valheim 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 Valheim 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 Valheim 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 these log lines:

Game server connected
Opened Steam server

Check Valheim 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 Valheim start can spend time generating a world. The service accepts connections after Opened Steam server appears.

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