feat(nixos): add SteamCMD server support

This commit is contained in:
2026-09-09 21:25:03 -04:00
parent b7ce5deb4c
commit 863dce5048
3 changed files with 457 additions and 226 deletions

View File

@@ -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.