diff --git a/nixos/modules/services/games/minecraft-server.nix b/nixos/modules/services/games/minecraft-server.nix
index f50d2897843..7d26d150165 100644
--- a/nixos/modules/services/games/minecraft-server.nix
+++ b/nixos/modules/services/games/minecraft-server.nix
@@ -4,8 +4,41 @@ with lib;
let
cfg = config.services.minecraft-server;
-in
-{
+
+ # We don't allow eula=false anyways
+ eulaFile = builtins.toFile "eula.txt" ''
+ # eula.txt managed by NixOS Configuration
+ eula=true
+ '';
+
+ whitelistFile = pkgs.writeText "whitelist.json"
+ (builtins.toJSON
+ (mapAttrsToList (n: v: { name = n; uuid = v; }) cfg.whitelist));
+
+ cfgToString = v: if builtins.isBool v then boolToString v else toString v;
+
+ serverPropertiesFile = pkgs.writeText "server.properties" (''
+ # server.properties managed by NixOS configuration
+ '' + concatStringsSep "\n" (mapAttrsToList
+ (n: v: "${n}=${cfgToString v}") cfg.serverProperties));
+
+
+ # To be able to open the firewall, we need to read out port values in the
+ # server properties, but fall back to the defaults when those don't exist.
+ # These defaults are from https://minecraft.gamepedia.com/Server.properties#Java_Edition_3
+ defaultServerPort = 25565;
+
+ serverPort = cfg.serverProperties.server-port or defaultServerPort;
+
+ rconPort = if cfg.serverProperties.enable-rcon or false
+ then cfg.serverProperties."rcon.port" or 25575
+ else null;
+
+ queryPort = if cfg.serverProperties.enable-query or false
+ then cfg.serverProperties."query.port" or 25565
+ else null;
+
+in {
options = {
services.minecraft-server = {
@@ -13,10 +46,32 @@ in
type = types.bool;
default = false;
description = ''
- If enabled, start a Minecraft Server. The listening port for
- the server is always 25565. The server
+ If enabled, start a Minecraft Server. The server
data will be loaded from and saved to
- ${cfg.dataDir}.
+ .
+ '';
+ };
+
+ declarative = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to use a declarative Minecraft server configuration.
+ Only if set to true, the options
+ and
+ will be
+ applied.
+ '';
+ };
+
+ eula = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether you agree to
+
+ Mojangs EULA. This option must be set to
+ true to run Minecraft server.
'';
};
@@ -24,7 +79,7 @@ in
type = types.path;
default = "/var/lib/minecraft";
description = ''
- Directory to store minecraft database and other state/data files.
+ Directory to store Minecraft database and other state/data files.
'';
};
@@ -32,21 +87,84 @@ in
type = types.bool;
default = false;
description = ''
- Whether to open ports in the firewall (if enabled) for the server.
+ Whether to open ports in the firewall for the server.
'';
};
+ whitelist = mkOption {
+ type = let
+ minecraftUUID = types.strMatching
+ "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
+ description = "Minecraft UUID";
+ };
+ in types.attrsOf minecraftUUID;
+ default = {};
+ description = ''
+ Whitelisted players, only has an effect when
+ is
+ true and the whitelist is enabled
+ via by
+ setting white-list to true.
+ This is a mapping from Minecraft usernames to UUIDs.
+ You can use to get a
+ Minecraft UUID for a username.
+ '';
+ example = literalExample ''
+ {
+ username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+ };
+ '';
+ };
+
+ serverProperties = mkOption {
+ type = with types; attrsOf (either bool (either int str));
+ default = {};
+ example = literalExample ''
+ {
+ server-port = 43000;
+ difficulty = 3;
+ gamemode = 1;
+ max-players = 5;
+ motd = "NixOS Minecraft server!";
+ white-list = true;
+ enable-rcon = true;
+ "rcon.password" = "hunter2";
+ }
+ '';
+ description = ''
+ Minecraft server properties for the server.properties file. Only has
+ an effect when
+ is set to true. See
+
+ for documentation on these values.
+ '';
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.minecraft-server;
+ defaultText = "pkgs.minecraft-server";
+ example = literalExample "pkgs.minecraft-server_1_12_2";
+ description = "Version of minecraft-server to run.";
+ };
+
jvmOpts = mkOption {
- type = types.str;
+ type = types.separatedString " ";
default = "-Xmx2048M -Xms2048M";
- description = "JVM options for the Minecraft Service.";
+ # Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
+ example = "-Xmx2048M -Xms4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
+ + "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
+ + "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
+ description = "JVM options for the Minecraft server.";
};
};
};
config = mkIf cfg.enable {
+
users.users.minecraft = {
- description = "Minecraft Server Service user";
+ description = "Minecraft server service user";
home = cfg.dataDir;
createHome = true;
uid = config.ids.uids.minecraft;
@@ -57,17 +175,60 @@ in
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
- serviceConfig.Restart = "always";
- serviceConfig.User = "minecraft";
- script = ''
- cd ${cfg.dataDir}
- exec ${pkgs.minecraft-server}/bin/minecraft-server ${cfg.jvmOpts}
- '';
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}";
+ Restart = "always";
+ User = "minecraft";
+ WorkingDirectory = cfg.dataDir;
+ };
+
+ preStart = ''
+ ln -sf ${eulaFile} eula.txt
+ '' + (if cfg.declarative then ''
+
+ if [ -e .declarative ]; then
+
+ # Was declarative before, no need to back up anything
+ ln -sf ${whitelistFile} whitelist.json
+ cp -f ${serverPropertiesFile} server.properties
+
+ else
+
+ # Declarative for the first time, backup stateful files
+ ln -sb --suffix=.stateful ${whitelistFile} whitelist.json
+ cp -b --suffix=.stateful ${serverPropertiesFile} server.properties
+
+ # server.properties must have write permissions, because every time
+ # the server starts it first parses the file and then regenerates it..
+ chmod +w server.properties
+ echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
+ > .declarative
+
+ fi
+ '' else ''
+ if [ -e .declarative ]; then
+ rm .declarative
+ fi
+ '');
};
- networking.firewall = mkIf cfg.openFirewall {
- allowedUDPPorts = [ 25565 ];
- allowedTCPPorts = [ 25565 ];
- };
+ networking.firewall = mkIf cfg.openFirewall (if cfg.declarative then {
+ allowedUDPPorts = [ serverPort ];
+ allowedTCPPorts = [ serverPort ]
+ ++ optional (! isNull queryPort) queryPort
+ ++ optional (! isNull rconPort) rconPort;
+ } else {
+ allowedUDPPorts = [ defaultServerPort ];
+ allowedTCPPorts = [ defaultServerPort ];
+ });
+
+ assertions = [
+ { assertion = cfg.eula;
+ message = "You must agree to Mojangs EULA to run minecraft-server."
+ + " Read https://account.mojang.com/documents/minecraft_eula and"
+ + " set `services.minecraft-server.eula` to `true` if you agree.";
+ }
+ ];
+
};
}