feat(nixos): add SteamCMD server support
This commit is contained in:
@@ -66,6 +66,45 @@
|
||||
|
||||
registry.address.pikachu.self = "10.23.10.102";
|
||||
|
||||
modules.services.steamcmd = {
|
||||
enable = true;
|
||||
dataDir = "/pikachu/data/steamcmd";
|
||||
apps.valheim = {
|
||||
enable = true;
|
||||
appId = 896660;
|
||||
installDir = "/pikachu/data/servers/valheim/main";
|
||||
workingDirectory = "/pikachu/data/servers/valheim/main";
|
||||
dataDirs = [ "/pikachu/media/games/servers/valheim/main" ];
|
||||
packages = [ pkgs.steam-run ];
|
||||
environment = {
|
||||
SteamAppId = "892970";
|
||||
SERVER_PASSWORD = "foobarbaz";
|
||||
};
|
||||
command = [
|
||||
"${pkgs.bash}/bin/bash"
|
||||
"-c"
|
||||
''
|
||||
${pkgs.coreutils}/bin/install -d -m 0770 /pikachu/media/games/servers/valheim/main
|
||||
export LD_LIBRARY_PATH="$PWD/linux64:$LD_LIBRARY_PATH"
|
||||
exec steam-run ./valheim_server.x86_64 \
|
||||
-nographics \
|
||||
-batchmode \
|
||||
-name main \
|
||||
-port 2456 \
|
||||
-world main \
|
||||
-savedir /pikachu/media/games/servers/valheim/main \
|
||||
-password "$SERVER_PASSWORD" \
|
||||
-public 0
|
||||
''
|
||||
];
|
||||
udpPorts = [
|
||||
2456
|
||||
2457
|
||||
2458
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
guests.actualbudget = {
|
||||
enable = true;
|
||||
vlan = 20;
|
||||
|
||||
325
configs/nixos/modules/services/steamcmd.nix
Normal file
325
configs/nixos/modules/services/steamcmd.nix
Normal file
@@ -0,0 +1,325 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.modules.services.steamcmd;
|
||||
|
||||
appType = lib.types.submodule (
|
||||
{ config, name, ... }:
|
||||
{
|
||||
options = {
|
||||
enable = lib.mkEnableOption "this SteamCMD app service";
|
||||
|
||||
appId = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
example = 896660;
|
||||
description = "Steam app ID for the dedicated server.";
|
||||
};
|
||||
|
||||
installDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "/data/servers/valheim/main";
|
||||
description = "Directory where SteamCMD installs the server files.";
|
||||
};
|
||||
|
||||
workingDirectory = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "/data/servers/valheim/main";
|
||||
description = "Directory where the game service starts.";
|
||||
};
|
||||
|
||||
command = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
example = [
|
||||
"/data/servers/valheim/main/valheim_server.x86_64"
|
||||
"-name"
|
||||
"main"
|
||||
];
|
||||
description = "Command that starts the game server.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = cfg.user;
|
||||
defaultText = lib.literalExpression "config.modules.services.steamcmd.user";
|
||||
description = "User account that runs the game service.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = cfg.group;
|
||||
defaultText = lib.literalExpression "config.modules.services.steamcmd.group";
|
||||
description = "Group account that owns writable server files.";
|
||||
};
|
||||
|
||||
environment = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
description = "Environment variables for the game service.";
|
||||
};
|
||||
|
||||
environmentFiles = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.path;
|
||||
default = [ ];
|
||||
description = "Environment files loaded by the game service.";
|
||||
};
|
||||
|
||||
dataDirs = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "/media/games/servers/valheim/main" ];
|
||||
description = "Additional writable directories created before the game service starts.";
|
||||
};
|
||||
|
||||
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.";
|
||||
};
|
||||
|
||||
packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.package;
|
||||
default = [ ];
|
||||
description = "Packages added to the game service path.";
|
||||
};
|
||||
|
||||
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 = {
|
||||
workingDirectory = lib.mkDefault config.installDir;
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
enabledApps = lib.filterAttrs (_: app: app.enable) cfg.apps;
|
||||
|
||||
prepareScript = name: app:
|
||||
pkgs.writeShellScript "steamcmd-${name}-prepare" ''
|
||||
set -eu
|
||||
|
||||
${pkgs.coreutils}/bin/install -d \
|
||||
-o ${lib.escapeShellArg cfg.user} \
|
||||
-g ${lib.escapeShellArg cfg.group} \
|
||||
-m 0770 \
|
||||
${lib.escapeShellArg cfg.dataDir}
|
||||
|
||||
${pkgs.coreutils}/bin/install -d \
|
||||
-o ${lib.escapeShellArg app.user} \
|
||||
-g ${lib.escapeShellArg app.group} \
|
||||
-m 0770 \
|
||||
${lib.escapeShellArg app.installDir} \
|
||||
${lib.escapeShellArgs app.dataDirs}
|
||||
'';
|
||||
|
||||
updateScript = name: app:
|
||||
pkgs.writeShellScript "steamcmd-${name}-update" ''
|
||||
set -eu
|
||||
|
||||
export HOME=${lib.escapeShellArg cfg.dataDir}
|
||||
exec ${lib.getExe cfg.package} \
|
||||
+force_install_dir ${lib.escapeShellArg app.installDir} \
|
||||
+login anonymous \
|
||||
+app_update ${toString app.appId} ${lib.optionalString app.validateUpdate "validate"} \
|
||||
+quit
|
||||
'';
|
||||
|
||||
startScript = name: app:
|
||||
pkgs.writeShellScript "steamcmd-${name}-start" ''
|
||||
set -eu
|
||||
|
||||
cd ${lib.escapeShellArg app.workingDirectory}
|
||||
exec ${lib.escapeShellArgs app.command}
|
||||
'';
|
||||
|
||||
restartCommand = name: app:
|
||||
if app.restart.command != null then
|
||||
app.restart.command
|
||||
else
|
||||
"systemctl restart ${lib.escapeShellArg name}.service";
|
||||
in
|
||||
{
|
||||
options.modules.services.steamcmd = {
|
||||
enable = lib.mkEnableOption "SteamCMD service management";
|
||||
|
||||
package = lib.mkPackageOption pkgs "steamcmd" { };
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "steamcmd";
|
||||
description = "User account that runs SteamCMD.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "steamcmd";
|
||||
description = "Group account that owns SteamCMD files.";
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/data/steamcmd";
|
||||
description = "Directory where SteamCMD stores metadata and cache files.";
|
||||
};
|
||||
|
||||
apps = lib.mkOption {
|
||||
type = lib.types.attrsOf appType;
|
||||
default = { };
|
||||
description = "SteamCMD-managed game services keyed by service name.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
registry."stable-uid".${config.networking.hostName}.steamcmd = 20003;
|
||||
}
|
||||
|
||||
(lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = enabledApps != { };
|
||||
message = "modules.services.steamcmd.apps must contain at least one enabled app.";
|
||||
}
|
||||
];
|
||||
|
||||
users.groups.${cfg.group} = { };
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
uid = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
|
||||
group = cfg.group;
|
||||
home = cfg.dataDir;
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules =
|
||||
[
|
||||
"d ${cfg.dataDir} 0770 ${cfg.user} ${cfg.group} - -"
|
||||
"z ${cfg.dataDir} 0770 ${cfg.user} ${cfg.group} - -"
|
||||
]
|
||||
++ lib.flatten (
|
||||
lib.mapAttrsToList (_: app: [
|
||||
"d ${app.installDir} 0770 ${cfg.user} ${cfg.group} - -"
|
||||
"z ${app.installDir} 0770 ${cfg.user} ${cfg.group} - -"
|
||||
]) enabledApps
|
||||
);
|
||||
|
||||
systemd.services = lib.mkMerge (
|
||||
lib.mapAttrsToList (name: app: {
|
||||
"steamcmd-${name}-update" = {
|
||||
description = "Update ${name} with SteamCMD";
|
||||
after = [ "network-online.target" ];
|
||||
wants = [ "network-online.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = app.user;
|
||||
Group = app.group;
|
||||
WorkingDirectory = "-${cfg.dataDir}";
|
||||
ExecStartPre = "+${prepareScript name app}";
|
||||
ExecStart = updateScript name app;
|
||||
};
|
||||
};
|
||||
|
||||
${name} = {
|
||||
description = "${name} game server";
|
||||
after = [
|
||||
"network-online.target"
|
||||
"steamcmd-${name}-update.service"
|
||||
];
|
||||
wants = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = app.packages;
|
||||
environment = app.environment;
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = app.user;
|
||||
Group = app.group;
|
||||
WorkingDirectory = "-${app.workingDirectory}";
|
||||
EnvironmentFile = app.environmentFiles;
|
||||
ExecStartPre = [
|
||||
"+${prepareScript name app}"
|
||||
] ++ lib.optional app.updateOnStart (updateScript name app);
|
||||
ExecStart = startScript name app;
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
|
||||
"steamcmd-${name}-restart" = lib.mkIf app.restart.enable {
|
||||
description = "Restart ${name}";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = pkgs.writeShellScript "steamcmd-${name}-restart" ''
|
||||
set -eu
|
||||
|
||||
exec ${restartCommand name app}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}) enabledApps
|
||||
);
|
||||
|
||||
systemd.timers = lib.mkMerge (
|
||||
lib.mapAttrsToList (name: app: {
|
||||
"steamcmd-${name}-restart" = lib.mkIf app.restart.enable {
|
||||
description = "Restart ${name} on a schedule";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = app.restart.onCalendar;
|
||||
RandomizedDelaySec = app.restart.randomizedDelaySec;
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
}) enabledApps
|
||||
);
|
||||
|
||||
networking.firewall.allowedTCPPorts = lib.flatten (lib.mapAttrsToList (_: app: app.tcpPorts) enabledApps);
|
||||
networking.firewall.allowedUDPPorts = lib.flatten (lib.mapAttrsToList (_: app: app.udpPorts) enabledApps);
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -1,226 +1,93 @@
|
||||
# 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/<game>.nix
|
||||
configs/nixos/guests/<game>.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.<game>`
|
||||
|
||||
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.<game>`
|
||||
|
||||
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/<game>/<instance-name>
|
||||
/media/games/servers/<game>/<instance-name>
|
||||
```
|
||||
|
||||
### `/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/<game>/<instance-name>`
|
||||
|
||||
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/<game>/<instance-name>`
|
||||
|
||||
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 <guest>` 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.<game>`.
|
||||
2. Map game-specific options to `modules.services.steamcmd.instances`.
|
||||
3. Add or update `guests.<game>`.
|
||||
4. Mount `/data/steamcmd` into the guest.
|
||||
5. Mount `/data/servers/<game>/<instance-name>` into the guest.
|
||||
6. Mount `/media/games/servers/<game>/<instance-name>` 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 <host>`.
|
||||
11. Check `container@<game>.service` on the host.
|
||||
12. Enter the guest with `nixos-container root-login <game>`.
|
||||
13. Check the game service with `systemctl status`.
|
||||
14. Follow logs with `journalctl -u <unit> -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.
|
||||
# Add a SteamCMD server
|
||||
|
||||
## 1. Configure the generic SteamCMD module
|
||||
|
||||
1. Create or update `configs/nixos/modules/services/steamcmd.nix`.
|
||||
2. Define `modules.services.steamcmd.enable`.
|
||||
3. Create the `steamcmd` service user.
|
||||
4. Configure the shared SteamCMD data directory.
|
||||
5. Add one option for each Steam app service.
|
||||
6. Run SteamCMD before each app service starts.
|
||||
7. Update the Steam app on each service restart.
|
||||
8. Add a restart timer with default settings.
|
||||
9. Add an option that overrides the restart command.
|
||||
10. Open declared in-machine firewall ports.
|
||||
|
||||
## 2. Configure the game service module
|
||||
|
||||
1. Create or update `configs/nixos/modules/services/<game>.nix`.
|
||||
2. Define `modules.services.<game>.enable`.
|
||||
3. Add the Steam app ID for the dedicated server.
|
||||
4. Add runtime packages that the server binary needs.
|
||||
5. Add `serverName` for the service identity.
|
||||
6. Use `serverName` as the default world or save name.
|
||||
7. Add a game-specific world or save option when the game supports it.
|
||||
8. Add guest-internal paths for server files and durable state.
|
||||
9. Add options for required ports.
|
||||
10. Add options for required secret files or values.
|
||||
11. Map the game options to `modules.services.steamcmd`.
|
||||
12. Configure the game service command.
|
||||
13. Pass secrets to the service without embedding secret values in Nix.
|
||||
14. Open the required in-machine firewall ports.
|
||||
|
||||
## 3. Configure the guest module
|
||||
|
||||
1. Create or update `configs/nixos/guests/<game>.nix`.
|
||||
2. Define `guests.<game>` options for host-level wiring.
|
||||
3. Add `serverName`.
|
||||
4. Pass `serverName` to `modules.services.<game>` inside `interior`.
|
||||
5. Add host path options for SteamCMD data, server files, and durable state.
|
||||
6. Use names such as `steamcmdPath`, `serverPath`, and `statePath`.
|
||||
7. Mount each host path into the guest.
|
||||
8. Mount sops-backed secret files read-only into the guest.
|
||||
9. Pass guest paths for secret files to `modules.services.<game>`.
|
||||
10. Set guest resource limits.
|
||||
11. Keep VLAN placement and address wiring in the guest or host configuration.
|
||||
|
||||
## 4. Add host storage
|
||||
|
||||
1. Choose a host path for SteamCMD data.
|
||||
2. Choose a host path for downloaded server files.
|
||||
3. Choose a host path for world data, saves, and generated config.
|
||||
4. Configure those paths through `guests.<game>`.
|
||||
|
||||
## 5. Add secrets
|
||||
|
||||
1. Add a sops secret for each durable server secret.
|
||||
2. Configure the guest option that names each sops secret.
|
||||
3. Do not put passwords or tokens in Nix strings.
|
||||
4. Do not pass passwords or tokens as process arguments.
|
||||
|
||||
## 6. Configure networking
|
||||
|
||||
1. Keep game port knowledge in `modules.services.<game>`.
|
||||
2. Open game ports from `modules.services.<game>`.
|
||||
3. Configure guest VLAN placement from the guest or host configuration.
|
||||
4. Add a DHCP reservation before you commit a stable guest address.
|
||||
5. Forward public ports to the guest address when the server needs public access.
|
||||
|
||||
## 7. Enable the guest on the target host
|
||||
|
||||
1. Set `guests.<game>.enable = true`.
|
||||
2. Set `guests.<game>.serverName` to the server identity.
|
||||
3. Set the host paths for SteamCMD data, server files, and durable state.
|
||||
4. Set the secret names for the server.
|
||||
5. Set the guest network options required by the host.
|
||||
|
||||
## 8. Deploy the host
|
||||
|
||||
1. Run `deploy-to <host>` from a clean checkout.
|
||||
2. Check `container@<game>.service` on the host.
|
||||
3. Enter the guest with `nixos-container root-login <game>`.
|
||||
4. Check the SteamCMD update service.
|
||||
5. Check the game service with `systemctl status <unit>`.
|
||||
6. Follow logs with `journalctl -u <unit> -f`.
|
||||
|
||||
## 9. Validate the server
|
||||
|
||||
1. Confirm the guest address with `machinectl status <game>`.
|
||||
2. Connect from the LAN before you add public forwarding.
|
||||
3. Restart the game service.
|
||||
4. Confirm that the restart updates the Steam app.
|
||||
5. Confirm that the restart timer uses the expected command.
|
||||
6. Confirm that durable state lands in the mounted state path.
|
||||
|
||||
Reference in New Issue
Block a user