# 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. ```nix 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/.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: ```nix 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: ```nix 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. ```nginx server { listen 2456 udp; proxy_pass 10.23.20.197:2456; } ``` Configure public DNS outside Nix: ```text valheim.alexion.dev -> public IP ``` Configure LAN DNS outside Nix: ```text valheim.alexion.dev -> reverse-proxy guest IP ``` Configure router forwarding outside Nix: ```text 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/.nix`. The guest module wraps `modules.services.games.`. 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: ```nix 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. ```nix guests.valheim.address = "10.23.20.197"; ``` becomes: ```nix registry.address.pikachu.valheim = "10.23.20.197"; ``` The guest route becomes: ```nix 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: ```yaml services: game-servers: password: ``` The Valheim guest defaults to: ```nix 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: ```sh cd configs/nixos sops-host secrets/shared.yaml ``` ## Validation Evaluate the host: ```sh nix eval .#nixosConfigurations.pikachu.config.system.build.toplevel.drvPath --raw ``` Deploy the host: ```sh deploy-to pikachu ``` Check the host secret: ```sh sudo test -s /run/secrets/services/game-servers/password && echo secret-ok ``` Check the guest: ```sh sudo systemctl status container@valheim.service --no-pager ``` Check the guest secret: ```sh sudo nixos-container run valheim -- test -s /run/secrets/services/game-servers/password ``` Check the game service: ```sh 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: ```text Game server connected Opened Steam server ``` Check game UDP listeners: ```sh sudo nixos-container run valheim -- ss -lunp | grep -E '2456|2457|valheim' ``` Check reverse-proxy UDP listeners: ```sh sudo nixos-container run reverse-proxy -- ss -lunp | grep -E '2456|2457|nginx' ``` Test the client through LAN DNS or public DNS: ```text valheim.alexion.dev:2456 ``` Test direct guest connectivity when ingress fails: ```text 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.