feat(nixos): run Valheim in a guest

This commit was merged in pull request #37.
This commit is contained in:
2026-09-10 10:16:27 -04:00
parent 5758a781d8
commit 8bd9ff166e
6 changed files with 158 additions and 23 deletions

View File

@@ -0,0 +1,116 @@
{
config,
lib,
...
}:
let
cfg = config.guests.valheim;
steamDir = "/data/steamcmd";
installDir = "/data/server";
stateDir = "/data/state";
in
{
options.guests.valheim = {
steamPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/steamcmd";
description = "Host path bind-mounted as SteamCMD's metadata directory.";
};
installPath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/valheim/server";
description = "Host path bind-mounted as Valheim's server install directory.";
};
statePath = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/srv/valheim/state";
description = "Host path bind-mounted as Valheim's world state directory.";
};
serverName = lib.mkOption {
type = lib.types.str;
default = "valheim";
description = "Name shown for the Valheim server.";
};
port = lib.mkOption {
type = lib.types.port;
default = 2456;
description = "Base UDP port for the Valheim server.";
};
passwordSecret = lib.mkOption {
type = lib.types.str;
default = "services/game-servers/password";
description = "SOPS secret name containing the Valheim server password.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.steamPath != null;
message = "guests.valheim.steamPath must point at the host directory that stores SteamCMD metadata.";
}
{
assertion = cfg.installPath != null;
message = "guests.valheim.installPath must point at the host directory that stores Valheim server files.";
}
{
assertion = cfg.statePath != null;
message = "guests.valheim.statePath must point at the host directory that stores Valheim world state.";
}
];
guests.valheim = {
interior.modules.services.games.valheim = {
enable = true;
inherit (cfg) serverName port;
inherit steamDir installDir stateDir;
passwordFile = "/run/secrets/${cfg.passwordSecret}";
};
mounts = {
${steamDir} = {
directory = cfg.steamPath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${installDir} = {
directory = cfg.installPath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${stateDir} = {
directory = cfg.statePath;
owner = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = "storage";
mode = "0770";
};
${"/run/secrets/${cfg.passwordSecret}"} = {
file = config.sops.secrets.${cfg.passwordSecret}.path;
readOnly = true;
};
};
limits = {
memory = lib.mkDefault "4G";
cpu = lib.mkDefault "400%";
tasksMax = lib.mkDefault 1024;
};
};
sops.secrets.${cfg.passwordSecret} = {
owner = "root";
group = "storage";
mode = "0440";
};
};
}

View File

@@ -65,21 +65,25 @@
}; };
registry.address.pikachu.self = "10.23.10.102"; registry.address.pikachu.self = "10.23.10.102";
registry.route.pikachu.self = {
domain = "valheim.alexion.dev";
udp.ports = [
config.modules.services.games.valheim.port
(config.modules.services.games.valheim.port + 1)
];
};
modules.services.games.valheim = { guests.valheim = {
enable = true; enable = true;
steamDir = "/pikachu/data/steamcmd"; vlan = 20;
installDir = "/pikachu/data/servers/valheim/main"; address = "10.23.20.197";
stateDir = "/pikachu/media/games/servers/valheim/main"; steamPath = "/pikachu/data/steamcmd";
installPath = "/pikachu/data/servers/valheim/main";
statePath = "/pikachu/media/games/servers/valheim/main";
serverName = "main"; serverName = "main";
password = "foobarbaz"; reverseProxy = {
enable = true;
route = {
domain = "valheim.alexion.dev";
udp.ports = [
config.guests.valheim.port
(config.guests.valheim.port + 1)
];
};
};
}; };
guests.actualbudget = { guests.actualbudget = {

View File

@@ -42,6 +42,10 @@ in
users.groups.storage.gid = 10000; users.groups.storage.gid = 10000;
security.sudo.extraConfig = ''
Defaults env_keep += "EDITOR VISUAL"
'';
time.timeZone = "America/New_York"; time.timeZone = "America/New_York";
i18n.defaultLocale = "en_GB.UTF-8"; i18n.defaultLocale = "en_GB.UTF-8";
@@ -87,6 +91,10 @@ in
update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}"; update = "sudo nixos-rebuild switch --flake .#${config.networking.hostName}";
cleanup = "sudo nix-collect-garbage -d"; cleanup = "sudo nix-collect-garbage -d";
sops-host = {
command = "sudo --preserve-env=EDITOR,VISUAL env SOPS_AGE_KEY_FILE=/var/lib/sops-nix/key.txt nix shell nixpkgs#sops nixpkgs#neovim -c sops";
abbreviation = false;
};
}; };
modules.shell.fish.enable = lib.mkDefault true; modules.shell.fish.enable = lib.mkDefault true;

View File

@@ -1,6 +1,7 @@
{ {
config, config,
lib, lib,
pkgs,
... ...
}: }:
{ {
@@ -37,9 +38,9 @@
description = "Base UDP port for the Valheim server."; description = "Base UDP port for the Valheim server.";
}; };
password = lib.mkOption { passwordFile = lib.mkOption {
type = lib.types.str; type = lib.types.path;
description = "Valheim server password."; description = "Runtime file containing the Valheim server password.";
}; };
}; };
@@ -47,6 +48,12 @@
config = lib.mkIf config.modules.services.games.valheim.enable ( config = lib.mkIf config.modules.services.games.valheim.enable (
let let
service = config.modules.services.games.valheim; service = config.modules.services.games.valheim;
startScript = pkgs.writeShellScript "valheim-start" ''
set -eu
password="$(${pkgs.coreutils}/bin/tr -d '\n' < ${lib.escapeShellArg service.passwordFile})"
exec ./valheim_server.x86_64 "$@" -password "$password"
'';
in in
{ {
modules.services.steamcmd = { modules.services.steamcmd = {
@@ -54,7 +61,7 @@
inherit (service) steamDir installDir stateDir; inherit (service) steamDir installDir stateDir;
serviceName = "valheim"; serviceName = "valheim";
appId = 896660; appId = 896660;
executable = "./valheim_server.x86_64"; executable = "${startScript}";
arguments = [ arguments = [
"-nographics" "-nographics"
"-batchmode" "-batchmode"
@@ -66,8 +73,6 @@
service.serverName service.serverName
"-savedir" "-savedir"
service.stateDir service.stateDir
"-password"
service.password
"-public" "-public"
"0" "0"
]; ];

View File

@@ -113,11 +113,11 @@
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
"d ${service.steamDir} 0770 ${user} ${group} - -" "d ${service.steamDir} 0770 ${user} ${group} - -"
"z ${service.steamDir} 0770 ${user} ${group} - -" "Z ${service.steamDir} 0770 ${user} ${group} - -"
"d ${service.installDir} 0770 ${user} ${group} - -" "d ${service.installDir} 0770 ${user} ${group} - -"
"z ${service.installDir} 0770 ${user} ${group} - -" "Z ${service.installDir} 0770 ${user} ${group} - -"
"d ${service.stateDir} 0770 ${user} ${group} - -" "d ${service.stateDir} 0770 ${user} ${group} - -"
"z ${service.stateDir} 0770 ${user} ${group} - -" "Z ${service.stateDir} 0770 ${user} ${group} - -"
]; ];
systemd.services = { systemd.services = {

View File

@@ -4,6 +4,8 @@ services:
users: users:
alexion: alexion:
password: ENC[AES256_GCM,data:SSyo2l6tCjgjxIuCEREIxU+inw2WHtJJR0/IEKUqefFfuAdA5Nf+lH/jfPw=,iv:9225w1Yur3KL3dBr1ct1aPvDdVJ94O8q9qAUiRrj8Nc=,tag:qZgx4XieVq4BcxkTZc0KGQ==,type:str] password: ENC[AES256_GCM,data:SSyo2l6tCjgjxIuCEREIxU+inw2WHtJJR0/IEKUqefFfuAdA5Nf+lH/jfPw=,iv:9225w1Yur3KL3dBr1ct1aPvDdVJ94O8q9qAUiRrj8Nc=,tag:qZgx4XieVq4BcxkTZc0KGQ==,type:str]
game-servers:
password: ENC[AES256_GCM,data:Wtq03MLRnc6/,iv:HhbiP8ZcltVGbCNDdP/gH5mCZXhVCTFv1DGzfrrgQG8=,tag:srsqcBQSD/HU8zFsuFimJQ==,type:str]
sops: sops:
age: age:
- enc: | - enc: |
@@ -42,7 +44,7 @@ sops:
28uWUpKzzgErq1aEbKNgh4cU5TH2RlAHjtzRC3ARUu9hlW6lnB4pSw== 28uWUpKzzgErq1aEbKNgh4cU5TH2RlAHjtzRC3ARUu9hlW6lnB4pSw==
-----END AGE ENCRYPTED FILE----- -----END AGE ENCRYPTED FILE-----
recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh recipient: age1wf5s0n0tgt6ld2ysgu9dc67mj8ylwecgl4utzg7hqwy3kut9zyms7aglmh
lastmodified: "2026-08-17T00:30:50Z" lastmodified: "2026-09-10T13:40:57Z"
mac: ENC[AES256_GCM,data:bNOsakxhrlO1quwLGA8inruHZ6tFpUo6cKMlV10jkJ75GOFzl5jzflGIuPfyncJCtIaZRBA2tS17TpGEEJozmbB2xQkeu67NmzOpEdoJzZWs/bY0FslbpGf8aGAd15BjOYNyMMNFavt0anRgEnsYXTDCflDCHLg1wvAvwtgRGYs=,iv:kfpzzWn3BHkSTQ8qpcD9lYn4wnZ/3nj/41SR25vUahE=,tag:8D1LSEBJIQcwYK9ETomwIw==,type:str] mac: ENC[AES256_GCM,data:3nu+ho3TNDnGE55LIaHqJxlMs1hWVK2wwiJiBIDH0jop9Wo/AgaUXzwXY1MwMXJHhGrkcjhwWWJdEgxX88NNFTc59b4kLtRqE3bfNXoQ3y3giyDMFyVE9YCNd4jyl+pLhpVSZA/+TKEIBpdZRwaXHrYSaLtysg/OdzXing0l3SM=,iv:C/vZ6X1HikuBn+pWTlPIfCfg5mtW2i4P6XLwN1VwF6k=,tag:UQl10fueTwCtXuirP5+C/g==,type:str]
unencrypted_suffix: _unencrypted unencrypted_suffix: _unencrypted
version: 3.13.3 version: 3.13.3