diff --git a/configs/nixos/hosts/pikachu/default.nix b/configs/nixos/hosts/pikachu/default.nix index 9172899..ce98f0a 100644 --- a/configs/nixos/hosts/pikachu/default.nix +++ b/configs/nixos/hosts/pikachu/default.nix @@ -66,6 +66,37 @@ registry.address.pikachu.self = "10.23.10.102"; + 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 + 2458 + ]; + }; + guests.actualbudget = { enable = true; vlan = 20; diff --git a/configs/nixos/modules/services/steamcmd.nix b/configs/nixos/modules/services/steamcmd.nix new file mode 100644 index 0000000..3172af3 --- /dev/null +++ b/configs/nixos/modules/services/steamcmd.nix @@ -0,0 +1,165 @@ +{ + config, + lib, + pkgs, + ... +}: +{ + options.modules.services.steamcmd = { + enable = lib.mkEnableOption "SteamCMD-managed game server"; + + steamDir = lib.mkOption { + type = lib.types.str; + example = "/data/steamcmd"; + description = "Directory where SteamCMD stores metadata and cache files."; + }; + + serviceName = lib.mkOption { + type = lib.types.str; + example = "valheim"; + description = "Name of the systemd service that runs the game server."; + }; + + appId = lib.mkOption { + type = lib.types.ints.positive; + example = 896660; + description = "Steam app ID for the dedicated server package."; + }; + + installDir = lib.mkOption { + type = lib.types.str; + example = "/data/servers/valheim/main"; + description = "Directory where SteamCMD installs the server files and where the game service starts."; + }; + + stateDir = lib.mkOption { + type = lib.types.str; + example = "/data/state/valheim/main"; + description = "Writable directory for durable game server state."; + }; + + executable = lib.mkOption { + type = lib.types.str; + example = "./valheim_server.x86_64"; + description = "Server executable, relative to installDir or absolute."; + }; + + arguments = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ + "-name" + "main" + ]; + description = "Arguments passed to the server executable."; + }; + + udpPorts = lib.mkOption { + type = lib.types.listOf lib.types.port; + default = [ ]; + description = "UDP ports opened for the game service."; + }; + }; + + config = lib.mkMerge [ + { + registry."stable-uid".${config.networking.hostName}.steamcmd = 20003; + } + + (lib.mkIf config.modules.services.steamcmd.enable ( + let + service = config.modules.services.steamcmd; + user = "steamcmd"; + group = "storage"; + + prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" '' + set -eu + + ${pkgs.coreutils}/bin/install -d \ + -o ${user} \ + -g ${group} \ + -m 0770 \ + ${lib.escapeShellArg service.steamDir} \ + ${lib.escapeShellArg service.installDir} \ + ${lib.escapeShellArg service.stateDir} + ''; + + updateScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-update" '' + set -eu + + export HOME=${lib.escapeShellArg service.steamDir} + exec ${lib.getExe pkgs.steamcmd} \ + +force_install_dir ${lib.escapeShellArg service.installDir} \ + +login anonymous \ + +app_update ${toString service.appId} validate \ + +quit + ''; + + startScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-start" '' + set -eu + + cd ${lib.escapeShellArg service.installDir} + export LD_LIBRARY_PATH=linux64:''${LD_LIBRARY_PATH-} + exec ${lib.getExe pkgs.steam-run} ${lib.escapeShellArg service.executable} ${lib.escapeShellArgs service.arguments} + ''; + in + { + users.users.${user} = { + isSystemUser = true; + uid = config.registry."stable-uid".${config.networking.hostName}.steamcmd; + inherit group; + home = service.steamDir; + }; + + systemd.tmpfiles.rules = [ + "d ${service.steamDir} 0770 ${user} ${group} - -" + "z ${service.steamDir} 0770 ${user} ${group} - -" + "d ${service.installDir} 0770 ${user} ${group} - -" + "z ${service.installDir} 0770 ${user} ${group} - -" + "d ${service.stateDir} 0770 ${user} ${group} - -" + "z ${service.stateDir} 0770 ${user} ${group} - -" + ]; + + systemd.services = { + "steamcmd-${service.serviceName}-update" = { + description = "Update ${service.serviceName} with SteamCMD"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + User = user; + Group = group; + WorkingDirectory = "-${service.steamDir}"; + ExecStartPre = "+${prepareScript}"; + ExecStart = updateScript; + }; + }; + + ${service.serviceName} = { + description = "${service.serviceName} game server"; + after = [ + "network-online.target" + "steamcmd-${service.serviceName}-update.service" + ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + User = user; + Group = group; + WorkingDirectory = "-${service.installDir}"; + ExecStartPre = [ + "+${prepareScript}" + updateScript + ]; + ExecStart = startScript; + Restart = "on-failure"; + }; + }; + }; + + networking.firewall.allowedUDPPorts = service.udpPorts; + } + )) + ]; +} diff --git a/docs/00 INBOX/ai/steamcmd-server.md b/docs/00 INBOX/ai/steamcmd-server.md deleted file mode 100644 index 6893d43..0000000 --- a/docs/00 INBOX/ai/steamcmd-server.md +++ /dev/null @@ -1,226 +0,0 @@ -# SteamCMD server notes - -These notes describe the emerging pattern for SteamCMD-based game servers in this monorepo. -They come from the Valheim prototype on Pikachu. - -## Goal - -Use NixOS to manage server operations, not game content. - -Nix owns service wiring, launch flags, ports, timers, users, and mounts. -SteamCMD owns downloaded server files. -The game owns world data, generated config, saves, and modded files. - -## Layering - -Use three layers for SteamCMD-based servers. - -```text -configs/nixos/modules/services/steamcmd.nix -configs/nixos/modules/services/.nix -configs/nixos/guests/.nix -``` - -### `modules.services.steamcmd` - -This module provides the generic SteamCMD machinery. - -It runs on any NixOS system. -That system can be a physical host or a guest. - -It owns: - -- the `steamcmd` service user, -- the shared SteamCMD data directory, -- per-instance Steam app updates, -- per-instance game service units, -- optional hard restart timers, -- firewall openings for declared ports. - -It does not know Valheim, PalWorld, or Project Zomboid semantics. -Game modules translate game-specific settings into SteamCMD instances. - -### `modules.services.` - -A game service module wraps `modules.services.steamcmd`. - -For Valheim, this layer owns: - -- Steam app id `896660`, -- the Valheim runtime app id, -- launch flags such as `-name`, `-world`, and `-port`, -- the expected UDP port range, -- the mapping from instance options to a SteamCMD instance. - -This layer exposes operational config only. -It avoids owning mutable gameplay files unless the game exposes stable launch flags. - -### `guests.` - -A guest module turns a game service into a deployable guest. - -It owns host-facing concerns: - -- VLAN placement, -- guest address registry entry, -- host paths and bind mounts, -- resource limits, -- secret file mounts, -- game-family instance declarations. - -Do not add a reusable `guests.steamcmd` base guest. -The guest auto-loader turns `configs/nixos/guests/steamcmd.nix` into a concrete guest. -A specialized guest cannot use it as a base without creating another container. - -## Instance model - -Use one guest per game family. -Use named instances inside that guest. - -For example, `guests.palworld` can contain `instances.current` and `instances.old`. -Each instance has its own ports and paths. -The host config must avoid clashes. - -For Valheim, the prototype uses `instances.foo`. -The instance name is the operational identity. -The world name defaults to the instance name. - -Keep these names separate in the module interface: - -```text -instance name -> unit names and paths -worldName -> game launch flag -``` - -This permits a test instance to load a copied world later. - -## Storage layout - -Use three storage roots. - -```text -/data/steamcmd -/data/servers// -/media/games/servers// -``` - -### `/data/steamcmd` - -Store SteamCMD data and cache here. - -This directory contains mutable Steam metadata and download state. -Do not treat it as durable game state. - -### `/data/servers//` - -Store the installed dedicated server files here. - -SteamCMD writes these files. -Mods can also mutate this tree. -For that reason, use per-instance app installs. -Do not share one app install between modded or historical instances. - -### `/media/games/servers//` - -Store world data, saves, generated config, and durable game state here. - -Backups are out of scope for the first prototype. -This path gives a stable backup boundary later. - -## Updates - -Do not pin game server versions in Nix. -Steam controls the server build. - -Nix controls when updates run. -The prototype updates on service start. -A daily hard restart timer can pull updates during a maintenance window. - -The generic SteamCMD layer supports a restart timer. -The default timer command restarts the instance service. -Game modules or host config can override that command later. - -Avoid generic player-warning or player-count logic in the SteamCMD layer. -Those flows are game-specific. - -## Secrets - -Use sops-backed environment files for durable configuration. -Mount them read-only into the guest. - -The Valheim prototype temporarily supports a hardcoded password. -That password leaks into the Nix store and process arguments. -Use it only for throwaway tests. - -The target model is: - -```text -host sops secret -> read-only guest mount -> systemd EnvironmentFile -``` - -For Valheim, the environment file contains: - -```sh -SERVER_PASSWORD=example-password -``` - -## Networking - -Open ports in the guest firewall from the game module. - -Valheim uses the base UDP port through `basePort + 2`. -The default base port is `2456`, so open UDP `2456-2458`. - -For public access, forward those UDP ports to the guest address. -DNS is optional for the first deployment. - -Use `machinectl status ` to confirm the current DHCP address. -Do not trust a guessed address. -Add a DHCP reservation before depending on a committed address. - -## Valheim prototype - -The prototype proved these facts: - -- Valheim runs inside a NixOS guest on Pikachu. -- SteamCMD installs the dedicated server files at service start. -- `steam-run` runs the mutable Valheim server binary. -- LAN clients can connect to the guest address and UDP port. -- The storage split works for SteamCMD data, server files, and world data. - -The prototype used: - -```text -guest: valheim -instance: foo -password: foobarbaz -actual DHCP address during test: 10.23.20.197 -``` - -The hardcoded password and test instance name need cleanup before a final PR. - -## Bring-up checklist - -1. Add or update `modules.services.`. -2. Map game-specific options to `modules.services.steamcmd.instances`. -3. Add or update `guests.`. -4. Mount `/data/steamcmd` into the guest. -5. Mount `/data/servers//` into the guest. -6. Mount `/media/games/servers//` into the guest. -7. Add a sops environment file for secrets. -8. Open the required game ports in the guest firewall. -9. Enable the guest on the target host. -10. Deploy with `deploy-to `. -11. Check `container@.service` on the host. -12. Enter the guest with `nixos-container root-login `. -13. Check the game service with `systemctl status`. -14. Follow logs with `journalctl -u -f`. -15. Connect from LAN before adding public forwarding. - -## Open cleanup items - -- Replace hardcoded prototype passwords with sops environment files. -- Choose durable instance names before long-term use. -- Add DHCP reservations for game guests. -- Add backup policy for world and modded server data. -- Decide whether server install trees need backup for modded servers.