# Enhanced Minecraft Server Module # # This module provides: # - A wrapper around the Infinidoge/nix-minecraft module # - Declarative configuration for multiple servers, including memory, operators, whitelist, and symlinks # - Automatic systemd service and timer generation for scheduled jobs # - Prebuilt helper scripts (rcon, query, backup, say, backup-routine) for server administration # - Support for backups, logging, and scheduled maintenance tasks # # Configuration Options: # enable – Enable the enhanced Minecraft servers (boolean) # user – System user that owns and runs the servers (string) - DON'T CHANGE THIS # group – System group that owns and runs the servers (string) - DON'T CHANGE THIS # dataDir – Directory to store Minecraft server data (path) # servers – Attribute set of servers, keyed by name. Each server can define: # memory.min / memory.max – JVM memory allocation (strings, e.g. "2G") # package – Minecraft server package to use (package) # autoStart – Start server at boot (boolean) # whitelist – Declarative whitelist (UUIDs per user) # operators – Declarative operator list with permission levels # symlinks – Files or packages symlinked into the server data directory # properties – Declarative `server.properties` values (ports, motd, difficulty, etc.) # schedules – Declarative scheduled jobs with systemd timers and services # # # Info: # I am happy to help if you have Issues and am happy to see a PR for any change # I ll use it personally too so you can expect frequent updates # # # ToDo: # Schedule to restart a server using systemD. I have not figured that out yet. # what i do know is that `sudo` does not work neither does it work if you just tell the server to restart. # { config, lib, pkgs, ... }: with lib; let cfg = config.nyx-minecraft.service; minecraftUUID = types.strMatching "[0-9a-fA-F-]{36}"; # Setup the Scripts for the server scriptDir = ./Scripts; mkScript = serverName: serverCfg: scriptType: let templateFile = scriptDir + "/minecraft-template-${scriptType}.sh"; rawText = builtins.readFile templateFile; scriptText = builtins.replaceStrings [ "@DATA_DIR@" "@RSYNC_BIN@" "@MCSTATUS_BIN@" "@MCRCON_BIN@" "@AWK_BIN@" "@QUERY_PORT@" "@RCON_PORT@" "@RCON_PASSWORD@" "@SERVER_NAME@" "@TAR_BIN@" "@ZIP_BIN@" "@UNZIP_BIN@" "@GZIP_BIN@" "@ZSTD_BIN@" "@PV_BIN@" "@DU_BIN@" "@BZIP2_BIN@" "@XZ_BIN@" ] # If you add anything here make sure to add it at the systemd service too [ cfg.dataDir "${pkgs.rsync}/bin/rsync" "${pkgs.mcstatus}/bin/mcstatus" "${pkgs.mcrcon}/bin/mcrcon" "${pkgs.gawk}/bin/awk" (toString (serverCfg.properties.serverPort + 200)) (toString (serverCfg.properties.serverPort + 100)) serverCfg.properties.rconPassword serverName "${pkgs.gnutar}/bin/tar" "${pkgs.zip}/bin/zip" "${pkgs.unzip}/bin/unzip" "${pkgs.gzip}/bin/gzip" "${pkgs.zstd}/bin/zstd" "${pkgs.pv}/bin/pv" "${pkgs.coreutils}/bin/du" "${pkgs.bzip2}/bin/bzip2" "${pkgs.xz}/bin/xz" ] rawText; in pkgs.writeShellScriptBin "minecraft-${serverName}-${scriptType}" scriptText; in { # Note most of the options get directly exposed # to nix-minecraft which makes it almost an # Drop in replacement options.nyx-minecraft.service = { enable = mkEnableOption "Enable enhanced Minecraft servers with backup, logging, and admin helpers."; eula = mkEnableOption '' Whether you agree to Mojang's EULA. This option must be set to true to run Minecraft server. ''; user = mkOption { type = types.str; default = "minecraft"; description = '' Name of the user to create and run servers under. It is recommended to leave this as the default, as it is the same user as . ''; internal = true; visible = false; }; group = mkOption { type = types.str; default = "minecraft"; description = '' Name of the group to create and run servers under. In order to modify the server files your user must be a part of this group. If you are using the tmux management system (the default), you also need to be a part of this group to attach to the tmux socket. It is recommended to leave this as the default, as it is the same group as . ''; }; dataDir = mkOption { type = types.path; default = "/srv/minecraft"; description = '' Directory to store the Minecraft servers. Each server will be under a subdirectory named after the server name in this directory, such as /srv/minecraft/servername. ''; }; servers = mkOption { type = types.attrsOf (types.submodule ({name, ...}: { options = { enable = mkEnableOption "Enable this Server"; memory = mkOption { type = types.submodule { options = { min = mkOption { type = types.str; default = "2G"; description = "Min JVM memory."; }; max = mkOption { type = types.str; default = "2G"; description = "Max JVM memory."; }; }; }; default = { min = "2G"; max = "2G"; }; description = "JVM memory settings for this server."; }; package = mkOption { description = "The Minecraft server package to use."; type = types.package; default = pkgs.minecraft-server; defaultText = literalExpression "pkgs.minecraft-server"; example = "pkgs.minecraftServers.vanilla-1_18_2"; }; autoStart = mkOption { type = types.bool; default = true; description = '' Whether to start this server on boot. If set to false, it can still be started with systemctl start minecraft-server-servername. Requires the server to be enabled. ''; }; whitelist = mkOption { type = types.attrsOf minecraftUUID; default = {}; description = '' Whitelisted players, only has an effect when enabled via by setting white-list to true. To use a non-declarative whitelist, enable the whitelist and don't fill in this value. As long as it is empty, no whitelist file is generated. ''; example = literalExpression '' { username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"; } ''; }; symlinks = mkOption { type = types.attrsOf (types.either types.path types.package); default = {}; description = '' Things to symlink into this server's data directory. Can be used to declaratively manage arbitrary files (e.g., mods, configs). ''; }; operators = mkOption { type = types.attrsOf ( types.coercedTo minecraftUUID (v: {uuid = v;}) ( types.submodule { options = { uuid = mkOption { type = minecraftUUID; description = "The operator's UUID"; example = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; }; level = mkOption { type = types.ints.between 0 4; description = "The operator's permission level"; default = 4; }; bypassesPlayerLimit = mkOption { type = types.bool; description = "If true, the operator can join the server even if the player limit has been reached"; default = false; }; }; } ) ); default = {}; description = "Operators with permission levels."; }; properties = mkOption { type = types.submodule { options = { serverPort = mkOption { type = types.int; default = 25565; description = "Server Port"; }; difficulty = mkOption { type = types.int; default = 2; description = "Difficulty in numbers: 0=Peaceful, 1=Easy, 2=Normal, 3=Hard"; }; gamemode = mkOption { type = types.int; default = 0; description = "Gamemode: 0=Survival, 1=Creative"; }; maxPlayers = mkOption { type = types.int; default = 5; description = "How many players can join the server"; }; motd = mkOption { type = types.str; default = "NixOS Minecraft server!"; description = "Message displayed when selecting the server"; }; rconPassword = mkOption { type = types.str; default = "superSecret"; description = "Password for Rcon"; }; hardcore = mkOption { type = types.bool; default = false; description = "Enable Hardcore mode"; }; levelSeed = mkOption { type = types.str; default = "42"; description = "World seed (default is the answer to the universe)"; }; }; }; default = {}; description = "Declarative Minecraft server.properties values."; }; # userActivity = { # enable = mkOption { # type = types.bool; # default = false; # description = '' # Enable periodic user activity logging for this server. # Writes to //UserActivity and is used by # backup --check-user. # ''; # }; # interval = mkOption { # type = types.str; # default = "5min"; # example = "1min"; # description = '' # How often user activity should be logged. # Uses systemd.time format (e.g. 30s, 1min, 5min). # ''; # }; # }; schedules = mkOption { type = types.attrsOf (types.submodule ({name, ...}: { options = { enable = mkOption { type = types.bool; default = true; description = "Whether this schedule is active."; }; timer = mkOption { type = types.str; example = "hourly"; description = "Systemd timer unit specifier (e.g., hourly, daily, weekly)."; }; code = mkOption { type = types.lines; description = "Shell code to execute when the schedule fires."; }; # Not properly enough tested #customEnviorment = mkOption { # type = types.lines; # description = "to expose binaries or other things to the SystemD service"; # example = "ZSTD_BIN=${pkgs.zstd}/bin/zstd"; #}; }; })); default = {}; description = "Scheduled jobs for this Minecraft server."; }; }; })); default = {}; description = "Servers to run under the enhanced Minecraft service."; }; }; # Wrapper for nix-minecraft config = mkIf cfg.enable { services.minecraft-servers = { enable = true; eula = cfg.eula; openFirewall = true; user = cfg.user; group = cfg.group; dataDir = cfg.dataDir; servers = lib.mapAttrs (serverName: serverCfg: { enable = serverCfg.enable; package = serverCfg.package; jvmOpts = "-Xmx${serverCfg.memory.max} -Xms${serverCfg.memory.min}"; autoStart = serverCfg.autoStart; symlinks = serverCfg.symlinks; whitelist = serverCfg.whitelist; operators = serverCfg.operators; serverProperties = { enable-rcon = true; enable-command-block = true; allow-flight = true; enable-query = true; server-port = serverCfg.properties.serverPort; difficulty = serverCfg.properties.difficulty; gamemode = serverCfg.properties.gamemode; max-players = serverCfg.properties.maxPlayers; motd = serverCfg.properties.motd; "rcon.password" = serverCfg.properties.rconPassword; "rcon.port" = serverCfg.properties.serverPort + 100; "query.port" = serverCfg.properties.serverPort + 200; hardcore = serverCfg.properties.hardcore; level-seed = serverCfg.properties.levelSeed; }; }) cfg.servers; }; # Schedule logic systemd.services = lib.mkMerge ( lib.mapAttrsToList ( serverName: serverCfg: lib.mapAttrs' (scheduleName: scheduleCfg: let # yes this will be building the scripts twice but thsi # way the path is accessible by the SystemD service rconBin = mkScript serverName serverCfg "rcon"; queryBin = mkScript serverName serverCfg "query"; backupBin = mkScript serverName serverCfg "backup"; sayBin = mkScript serverName serverCfg "say"; routineBin = mkScript serverName serverCfg "backup-routine"; in { name = "minecraft-${serverName}-${scheduleName}"; value = { description = "Minecraft ${serverName} scheduled job: ${scheduleName}"; serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; Environment = [ "RCON_BIN=${rconBin}/bin/minecraft-${serverName}-rcon" "QUERY_BIN=${queryBin}/bin/minecraft-${serverName}-query" "USERACTIVITY_BIN=${queryBin}/bin/minecraft-${serverName}-user-activity" "BACKUP_BIN=${backupBin}/bin/minecraft-${serverName}-backup" "SAY_BIN=${sayBin}/bin/minecraft-${serverName}-say" "ROUTINE_BIN=${routineBin}/bin/minecraft-${serverName}-backup-routine" "ZSTD_BIN=${pkgs.zstd}/bin/zstd" # add more bin here # Not properly enough tested # #scheduleCfg.customEnviorment ]; ExecStart = pkgs.writeShellScript "minecraft-${serverName}-${scheduleName}.sh" '' #!/usr/bin/env bash echo "hi — available helpers:" echo "if you want any custom scripts or" echo "packages for your script you need to ExecStart" echo " $RCON_BIN" echo " $QUERY_BIN" echo " $BACKUP_BIN" echo " $SAY_BIN" echo " $ROUTINE_BIN" echo " $USERACTIVITY_BIN" # this is so it can use the scripts the same way you would run them: minecraft-${serverName}-query() { $QUERY_BIN "$@"; } minecraft-${serverName}-rcon() { $RCON_BIN "$@"; } minecraft-${serverName}-backup() { $BACKUP_BIN "$@"; } minecraft-${serverName}-say() { $SAY_BIN "$@"; } minecraft-${serverName}-backup-routine() { $ROUTINE_BIN "$@"; } minecraft-${serverName}-user-activity() { $USERACTIVITY_BIN "$@"; } # her your code will go: ${scheduleCfg.code} ''; }; # If you have this on: # wantedBy = [ "multi-user.target" ]; # it will run the service on each rebuild. # i dont want that you can enable it if you like }; }) serverCfg.schedules ) cfg.servers ); # the timers to actually run the SystemD service systemd.timers = lib.mkMerge ( lib.mapAttrsToList ( serverName: serverCfg: lib.mapAttrs' (scheduleName: scheduleCfg: { name = "minecraft-${serverName}-${scheduleName}"; value = { description = "Timer for Minecraft ${serverName} schedule ${scheduleName}"; wantedBy = ["timers.target"]; timerConfig.OnCalendar = scheduleCfg.timer; }; }) serverCfg.schedules ) cfg.servers ); # systemd.services = lib.mkMerge ( # lib.mapAttrsToList (serverName: serverCfg: # lib.mkIf serverCfg.userActivity.enable { # "minecraft-${serverName}-user-activity" = { # description = "Minecraft ${serverName} user activity logger"; # serviceConfig = { # Type = "oneshot"; # User = cfg.user; # Group = cfg.group; # Environment = [ # "QUERY_BIN=${mkScript serverName serverCfg "query"}/bin/minecraft-${serverName}-query" # ]; # ExecStart = # "${mkScript serverName serverCfg "user-activity"}/bin/minecraft-${serverName}-user-activity"; # }; # }; # } # ) cfg.servers # ); # systemd.timers = lib.mkMerge ( # lib.mapAttrsToList (serverName: serverCfg: # lib.mkIf serverCfg.userActivity.enable { # "minecraft-${serverName}-user-activity" = { # description = "Timer for Minecraft ${serverName} user activity logging"; # wantedBy = [ "timers.target" ]; # timerConfig = { # OnBootSec = "2min"; # OnUnitActiveSec = serverCfg.userActivity.interval; # AccuracySec = "30s"; # }; # }; # } # ) cfg.servers # ); # this is building the scripts for the user # Those are the prewritten scripts from the ./Script dir environment.systemPackages = lib.flatten ( lib.mapAttrsToList (serverName: serverCfg: [ (mkScript serverName serverCfg "rcon") (mkScript serverName serverCfg "query") (mkScript serverName serverCfg "backup") (mkScript serverName serverCfg "say") (mkScript serverName serverCfg "backup-routine") (mkScript serverName serverCfg "user-activity") ]) cfg.servers ); }; }