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