{ config, lib, pkgs, ... }: let cfg = config.modules.services.steamcmd; runtimeAppId = if cfg.runtimeAppId == null then cfg.appId else cfg.runtimeAppId; libraryPath = lib.concatStringsSep ":" cfg.libraryDirs; prepareScript = pkgs.writeShellScript "steamcmd-${cfg.serviceName}-prepare" '' set -eu ${pkgs.coreutils}/bin/install -d \ -o ${lib.escapeShellArg cfg.user} \ -g ${lib.escapeShellArg cfg.group} \ -m 0770 \ ${lib.escapeShellArg cfg.steamDir} \ ${lib.escapeShellArg cfg.installDir} \ ${lib.escapeShellArg cfg.stateDir} ''; updateScript = pkgs.writeShellScript "steamcmd-${cfg.serviceName}-update" '' set -eu export HOME=${lib.escapeShellArg cfg.steamDir} exec ${lib.getExe cfg.package} \ +force_install_dir ${lib.escapeShellArg cfg.installDir} \ +login anonymous \ +app_update ${toString cfg.appId} ${lib.optionalString cfg.validateUpdate "validate"} \ +quit ''; startScript = pkgs.writeShellScript "steamcmd-${cfg.serviceName}-start" '' set -eu cd ${lib.escapeShellArg cfg.installDir} export SteamAppId=${lib.escapeShellArg (toString runtimeAppId)} ${lib.optionalString (cfg.libraryDirs != [ ]) ''export LD_LIBRARY_PATH=${lib.escapeShellArg libraryPath}:''${LD_LIBRARY_PATH-}''} exec ${lib.getExe pkgs.steam-run} ${lib.escapeShellArg cfg.executable} ${lib.escapeShellArgs cfg.arguments} ''; restartCommand = if cfg.restart.command != null then cfg.restart.command else "systemctl restart ${lib.escapeShellArg cfg.serviceName}.service"; in { options.modules.services.steamcmd = { enable = lib.mkEnableOption "SteamCMD-managed game server"; package = lib.mkPackageOption pkgs "steamcmd" { }; serviceName = lib.mkOption { type = lib.types.str; default = "steamcmd"; example = "valheim"; description = "Name of the systemd service that runs the game server."; }; user = lib.mkOption { type = lib.types.str; default = "steamcmd"; description = "User account that runs SteamCMD and the game server."; }; group = lib.mkOption { type = lib.types.str; default = "storage"; description = "Group account that owns writable server files."; }; steamDir = lib.mkOption { type = lib.types.str; default = "/data/steamcmd"; description = "Directory where SteamCMD stores metadata and cache files."; }; appId = lib.mkOption { type = lib.types.ints.positive; example = 896660; description = "Steam app ID for the dedicated server package."; }; runtimeAppId = lib.mkOption { type = lib.types.nullOr lib.types.ints.positive; default = null; example = 892970; description = "Steam app ID exposed to the running server through SteamAppId. Defaults to appId."; }; 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."; }; libraryDirs = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "linux64" ]; description = "Library directories added to LD_LIBRARY_PATH before starting the server."; }; environmentFiles = lib.mkOption { type = lib.types.listOf lib.types.path; default = [ ]; description = "Environment files loaded by the game service."; }; updateOnStart = lib.mkOption { type = lib.types.bool; default = true; description = "Runs SteamCMD before each game service start."; }; validateUpdate = lib.mkOption { type = lib.types.bool; default = true; description = "Passes validate to SteamCMD app_update."; }; tcpPorts = lib.mkOption { type = lib.types.listOf lib.types.port; default = [ ]; description = "TCP ports opened for the game service."; }; udpPorts = lib.mkOption { type = lib.types.listOf lib.types.port; default = [ ]; description = "UDP ports opened for the game service."; }; restart = { enable = lib.mkOption { type = lib.types.bool; default = true; description = "Enables the scheduled restart timer."; }; onCalendar = lib.mkOption { type = lib.types.str; default = "daily"; example = "*-*-* 05:00:00"; description = "Calendar expression for the restart timer."; }; randomizedDelaySec = lib.mkOption { type = lib.types.str; default = "15m"; description = "Maximum randomized delay for the restart timer."; }; command = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "systemctl restart valheim.service"; description = "Command run by the restart timer."; }; }; }; config = lib.mkMerge [ { registry."stable-uid".${config.networking.hostName}.steamcmd = 20003; } (lib.mkIf cfg.enable { users.groups.${cfg.group} = { }; users.users.${cfg.user} = { isSystemUser = true; uid = config.registry."stable-uid".${config.networking.hostName}.steamcmd; group = cfg.group; home = cfg.steamDir; }; systemd.tmpfiles.rules = [ "d ${cfg.steamDir} 0770 ${cfg.user} ${cfg.group} - -" "z ${cfg.steamDir} 0770 ${cfg.user} ${cfg.group} - -" "d ${cfg.installDir} 0770 ${cfg.user} ${cfg.group} - -" "z ${cfg.installDir} 0770 ${cfg.user} ${cfg.group} - -" "d ${cfg.stateDir} 0770 ${cfg.user} ${cfg.group} - -" "z ${cfg.stateDir} 0770 ${cfg.user} ${cfg.group} - -" ]; systemd.services = { "steamcmd-${cfg.serviceName}-update" = { description = "Update ${cfg.serviceName} with SteamCMD"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; WorkingDirectory = "-${cfg.steamDir}"; ExecStartPre = "+${prepareScript}"; ExecStart = updateScript; }; }; ${cfg.serviceName} = { description = "${cfg.serviceName} game server"; after = [ "network-online.target" "steamcmd-${cfg.serviceName}-update.service" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; path = [ pkgs.steam-run ]; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; WorkingDirectory = "-${cfg.installDir}"; EnvironmentFile = cfg.environmentFiles; ExecStartPre = [ "+${prepareScript}" ] ++ lib.optional cfg.updateOnStart updateScript; ExecStart = startScript; Restart = "on-failure"; }; }; "steamcmd-${cfg.serviceName}-restart" = lib.mkIf cfg.restart.enable { description = "Restart ${cfg.serviceName}"; serviceConfig = { Type = "oneshot"; ExecStart = pkgs.writeShellScript "steamcmd-${cfg.serviceName}-restart" '' set -eu exec ${restartCommand} ''; }; }; }; systemd.timers."steamcmd-${cfg.serviceName}-restart" = lib.mkIf cfg.restart.enable { description = "Restart ${cfg.serviceName} on a schedule"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = cfg.restart.onCalendar; RandomizedDelaySec = cfg.restart.randomizedDelaySec; Persistent = true; }; }; networking.firewall.allowedTCPPorts = cfg.tcpPorts; networking.firewall.allowedUDPPorts = cfg.udpPorts; }) ]; }