From 74226680838d5142eccb361b14d89786e82ccc30 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Mon, 8 Jun 2026 11:59:48 +0800 Subject: [PATCH 1/3] feat(nemesis): add gtnh desktop entry --- nix/modules/prismlauncher.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nix/modules/prismlauncher.nix b/nix/modules/prismlauncher.nix index c2b0faee..88c01688 100644 --- a/nix/modules/prismlauncher.nix +++ b/nix/modules/prismlauncher.nix @@ -10,8 +10,13 @@ in darwin.prismlauncher = osModule; nixos.prismlauncher = osModule; homeManager.prismlauncher = - { pkgs, ... }: + { config, pkgs, ... }: { + xdg.desktopEntries.gtnh = { + name = "GregTech New Horizons"; + icon = "${config.home.homeDirectory}/.local/share/PrismLauncher/instances/GT_New_Horizons_2.8.4_Java_17-25/icon.png"; + exec = "prismlauncher --launch GT_New_Horizons_2.8.4_Java_17-25"; + }; home.packages = [ (pkgs.prismlauncher.override { jdks = [ pkgs.jdk25 ]; From 9013db1131b2f046aad3c969f354d56113568bbd Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Mon, 8 Jun 2026 12:01:54 +0800 Subject: [PATCH 2/3] feat(gtnh-server): add minimal nemesis server --- docs/gtnh-server-guide.html | 652 ++++++++++++++++++++++++++++++++++ docs/gtnh-server-migration.md | 568 +++++++++++++++++++++++++++++ nix/hosts.nix | 2 + nix/modules/gtnh-backups.nix | 50 +++ nix/modules/gtnh-server.nix | 102 ++++++ 5 files changed, 1374 insertions(+) create mode 100644 docs/gtnh-server-guide.html create mode 100644 docs/gtnh-server-migration.md create mode 100644 nix/modules/gtnh-backups.nix create mode 100644 nix/modules/gtnh-server.nix diff --git a/docs/gtnh-server-guide.html b/docs/gtnh-server-guide.html new file mode 100644 index 00000000..d274f5e0 --- /dev/null +++ b/docs/gtnh-server-guide.html @@ -0,0 +1,652 @@ + + + + + + Nemesis GTNH Server Guide + + + +
+
+
nemesis is serving GT New Horizons
+

GTNH 2.8.4 Server Guide

+

This is the operating manual for the GT New Horizons server running on nemesis. It covers connecting from Prism, day-to-day administration, backups, restores, upgrades, and the parts of the NixOS setup that keep the server reproducible.

+
+
Connect address
nemesis:25565
+
LAN IP
10.10.0.11:25565
+
Tailscale IP
100.98.114.23:25565
+
World
Ad Infinitum
+
+
+ +
+ + +
+
+

1Quick start

+

The server is already installed, rebuilt into the nemesis NixOS configuration, started, listening on port 25565, and running the migrated Ad Infinitum world.

+
+
+

Join from Prism

+
    +
  1. Launch GT_New_Horizons_2.8.4_Java_17-25.
  2. +
  3. Open Multiplayer.
  4. +
  5. Add server nemesis:25565. If local DNS is not available, use 10.10.0.11:25565.
  6. +
  7. Join as wagoqi.
  8. +
+
+
+

Current access

+
    +
  • Whitelist is enabled.
  • +
  • wagoqi is whitelisted.
  • +
  • wagoqi is an operator at permission level 2.
  • +
  • Online-mode is enabled.
  • +
+
+
+
+

Client version must match exactly. Use GTNH 2.8.4, Java 17-25 client. A different GTNH version can fail during handshake or, worse, expose mismatched configs and scripts.

+
+
+ +
+

2What is running

+

The server is a native NixOS systemd service, not a Docker container and not a Prism-launched client instance. The mutable Minecraft state lives in /srv/gtnh; the service definition lives in this repo.

+ + + + + + + + + + + + + +
ItemValue
Hostnemesis
Servicegtnh-server.service
Console FIFO/run/gtnh-server.stdin
Server directory/srv/gtnh
World directory/srv/gtnh/world
Official server packGT_New_Horizons_2.8.4_Server_Java_17-25.zip
JavaNixOS jdk25_headless
Memory flags-Xms6G -Xmx10G -XX:+UseZGC
Startup jarlwjgl3ify-forgePatches.jar with @java9args.txt
+
+ TCP 25565 open + UDP 25565 open + daily host backup + systemd restart on failure +
+
+ +
+

3Client setup

+

Use the existing Prism instance

+

The migrated world came from this Prism instance:

+
~/.local/share/PrismLauncher/instances/GT_New_Horizons_2.8.4_Java_17-25
+

Use that exact instance when connecting. The server uses the official 2.8.4 Java 17-25 server pack, which is the matching server-side distribution for that client.

+ +

Addresses to try

+ + + + + + + +
Where you areAddressNotes
On the same LANnemesis:25565Preferred if hostname resolution works.
On the same LAN, no hostname10.10.0.11:25565Direct wired LAN address.
Over Tailscale100.98.114.23:25565Use when away from the LAN and Tailscale is connected.
+ +

First login validation

+
    +
  • Inventory, armor, Baubles, Thaumcraft data, and location should match the singleplayer save.
  • +
  • Quest progress should be present. The server log loaded BetterQuesting progress for two UUIDs.
  • +
  • Machines and multiblocks should be in place.
  • +
  • Dimensions should exist. The server loaded many Galacticraft and modded dimensions during first boot.
  • +
+
+

If inventory or position is wrong, stop immediately. Do not keep playing and saving. Singleplayer-to-server migrations can expose UUID/playerdata differences. The original save backup exists so playerdata can be repaired carefully.

+
+
+ +
+

4Server controls

+

Use systemd for process lifecycle. Use the console FIFO for in-game commands. Do not kill the Java process directly unless the service is stuck and a normal stop has failed.

+
+
sudo systemctl status gtnh-server.serviceShow service state, main PID, memory use, recent logs.
+
journalctl -fu gtnh-server.serviceFollow live server logs.
+
sudo systemctl start gtnh-server.serviceStart the server.
+
sudo systemctl stop gtnh-server.serviceGracefully stops by sending stop through the server console.
+
sudo systemctl restart gtnh-server.serviceGraceful stop followed by start. Use after config changes.
+
ss -ltnup | grep 25565Confirm the Minecraft listener is present.
+
+
+

The service has startup conditions for /srv/gtnh/java9args.txt and /srv/gtnh/lwjgl3ify-forgePatches.jar. If those files are missing, systemd skips the service instead of crash-looping a half-installed server.

+
+
+ +
+

5Console commands

+

Systemd owns a FIFO at /run/gtnh-server.stdin. Writing a line to it is the same as typing into the dedicated server console.

+
echo "say hello from nemesis" | sudo tee /run/gtnh-server.stdin
+ + + + + + + + + + +
CommandPurpose
echo "list" | sudo tee /run/gtnh-server.stdinList online players.
echo "say Server restart in 5 minutes" | sudo tee /run/gtnh-server.stdinBroadcast a message.
echo "save-all" | sudo tee /run/gtnh-server.stdinForce a world save.
echo "stop" | sudo tee /run/gtnh-server.stdinAsk Minecraft/Forge to stop cleanly.
echo "whitelist add USER" | sudo tee /run/gtnh-server.stdinAllow a player to join.
echo "op USER" | sudo tee /run/gtnh-server.stdinGrant operator privileges.
+
+

Prefer systemctl stop for routine shutdowns. The NixOS service has a graceful stop script that sends stop and waits up to 120 seconds before sending TERM.

+
+
+ +
+

6Players, whitelist, and ops

+

The server is private. white-list=true and online-mode=true are set in /srv/gtnh/server.properties.

+

Add a player

+
echo "whitelist add PLAYERNAME" | sudo tee /run/gtnh-server.stdin
+

Remove a player

+
echo "whitelist remove PLAYERNAME" | sudo tee /run/gtnh-server.stdin
+

Give or remove operator access

+
echo "op PLAYERNAME" | sudo tee /run/gtnh-server.stdin
+echo "deop PLAYERNAME" | sudo tee /run/gtnh-server.stdin
+

Current admin

+ + + + + +
PlayerUUIDState
wagoqi22c6c7d9-3c10-41a0-9e9d-759f2aeec75bWhitelisted and op level 2.
+
+ +
+

7World and state layout

+

The mutable state is intentionally outside the Nix store. The NixOS module defines the service; /srv/gtnh holds the actual server files, world, logs, backups, and generated configs.

+ + + + + + + + + + + +
PathWhat it is
/srv/gtnhServer root and working directory.
/srv/gtnh/worldThe migrated Ad Infinitum save, renamed to the server's level-name=world.
/srv/gtnh/server.propertiesMinecraft server settings. Keep level-name=world.
/srv/gtnh/logsGTNH/Forge log files in addition to journald.
/srv/gtnh/backupsServerUtilities' in-pack backups.
/srv/gtnh-backupsHost-level compressed backups produced by the NixOS timer.
~/gtnh-migration-backupsOriginal pre-migration save backup from the Prism world.
+ +

Important server.properties values

+
level-name=world
+level-type=rwg
+difficulty=3
+allow-flight=true
+enable-command-block=true
+white-list=true
+max-players=5
+server-port=25565
+motd=GT New Horizons 2.8.4 on nemesis
+
+

Do not rename /srv/gtnh/world or change level-name casually. If they stop matching, the server will create or load a different world.

+
+
+ +
+

8Backups

+

There are three useful backup layers. Keep all of them until the migrated world has been played and validated for a while.

+
+
+

Migration backup

+

Created before copying Ad Infinitum to the server.

+
~/gtnh-migration-backups/Ad-Infinitum-before-server-migration-*.tar.gz
+
+
+

GTNH ServerUtilities

+

In-pack backups written below the live server directory.

+
/srv/gtnh/backups
+
+
+

Host-level daily backup

+

NixOS timer archives /srv/gtnh to zstd-compressed tarballs.

+
/srv/gtnh-backups/gtnh-YYYYMMDD-HHMMSS.tar.zst
+
+
+

Manual copy before risky work

+

Before updates, config surgery, or NBT edits, take a named manual archive.

+
sudo systemctl start gtnh-backup.service
+
+
+ +

Check the timer

+
systemctl list-timers | grep gtnh
+sudo ls -lh /srv/gtnh-backups
+ +

Run a manual backup

+
sudo systemctl start gtnh-backup.service
+journalctl -u gtnh-backup.service -n 80 --no-pager
+
+

The host backup asks the server to save-all, waits 10 seconds, then archives /srv/gtnh. It excludes /srv/gtnh/backups to avoid backup recursion.

+
+
+ +
+

9Restore procedure

+

Restores should be boring and deliberate. Stop the server, move the damaged state aside, unpack the backup, fix ownership, and start again.

+
    +
  1. Announce downtime if players are online.
  2. +
  3. Stop the server cleanly.
  4. +
  5. Keep the current broken state instead of deleting it.
  6. +
  7. Unpack a known-good backup.
  8. +
  9. Restore ownership and permissions.
  10. +
  11. Start the server and watch logs until it reaches ready state.
  12. +
+
echo "say Server restore starting" | sudo tee /run/gtnh-server.stdin
+sudo systemctl stop gtnh-server.service
+sudo mv /srv/gtnh /srv/gtnh.broken.$(date +%Y%m%d-%H%M%S)
+sudo mkdir -p /srv/gtnh
+sudo tar -C /srv -I 'zstd -d' -xf /srv/gtnh-backups/gtnh-YYYYMMDD-HHMMSS.tar.zst
+sudo chown -R gtnh:gtnh /srv/gtnh
+sudo chmod -R u+rwX,g+rX,o-rwx /srv/gtnh
+sudo systemctl start gtnh-server.service
+journalctl -fu gtnh-server.service
+
+

Replace gtnh-YYYYMMDD-HHMMSS.tar.zst with the actual backup file. If zstd is missing in an ad-hoc shell, enter the repo dev shell or run through comma.

+
+
+ +
+

10Updating GTNH

+

Do not update casually. GTNH server and client versions must match exactly. The current installation is pinned operationally to GTNH 2.8.4 using the official Java 17-25 server pack.

+

Safe update shape

+
    +
  1. Read the GTNH migration notes for the target version.
  2. +
  3. Update the Prism client first in a copy of the instance, not the only playable instance.
  4. +
  5. Run sudo systemctl start gtnh-backup.service.
  6. +
  7. Stop the server.
  8. +
  9. Unpack the new official server pack into a staging directory.
  10. +
  11. Copy or merge only the state that belongs across versions, following GTNH docs.
  12. +
  13. Start and validate before deleting any old pack or backup.
  14. +
+
+

Never replace /srv/gtnh with a Prism client instance. Use the official server pack for the target version and migrate the world/state into it.

+
+
+ +
+

11Troubleshooting

+

Server does not show as online

+
systemctl status gtnh-server.service --no-pager -l
+ss -ltnup | grep 25565
+journalctl -u gtnh-server.service -n 160 --no-pager
+
    +
  • If the service is inactive with unmet conditions, check that /srv/gtnh/java9args.txt and /srv/gtnh/lwjgl3ify-forgePatches.jar exist.
  • +
  • If it is starting, wait. GTNH first boot can take several minutes.
  • +
  • If it crashed, inspect the journal and /srv/gtnh/crash-reports.
  • +
+ +

Client says mod mismatch

+

Use the exact GTNH 2.8.4 Java 17-25 Prism instance. Do not connect from a different GTNH version or a local instance with hand-edited mods/configs.

+ +

Whitelist rejection

+
echo "whitelist list" | sudo tee /run/gtnh-server.stdin
+echo "whitelist add PLAYERNAME" | sudo tee /run/gtnh-server.stdin
+ +

Wrong inventory or spawn after migration

+
    +
  1. Stop the server before more saves happen.
  2. +
  3. Keep the broken state for inspection.
  4. +
  5. Compare level.dat player data and world/playerdata/*.dat UUID data against the original Prism save.
  6. +
  7. Use an NBT editor carefully; do not improvise on the only copy.
  8. +
+ +

Lag after startup

+

Some initial lag is expected while GTNH loads dimensions and caches. If TPS remains bad, check memory and logs:

+
systemctl status gtnh-server.service --no-pager -l
+journalctl -u gtnh-server.service -n 200 --no-pager | grep -iE 'tps|tick|overload|warn|error'
+
+ +
+

12NixOS implementation

+

The NixOS pieces are in the repo so the service survives rebuilds and host reconfiguration. The Minecraft world remains mutable data under /srv/gtnh.

+ + + + + + + + +
Repo fileRole
nix/modules/gtnh-server.nixDefines the gtnh user/group, firewall ports, FIFO socket, Java command, service hardening, and graceful stop behavior.
nix/modules/gtnh-backups.nixDefines the daily backup service and timer.
nix/hosts.nixImports the GTNH server and backup modules on nemesis.
docs/gtnh-server-migration.mdOriginal migration plan and references.
+ +

Validate Nix changes

+
nix develop -c just format-nix
+nix develop -c just check-nix
+nix develop -c just rb
+
+

Rebuilds are host actions. Run just rb on nemesis for this server. New Nix files must be tracked by git before flake evaluation in this repo workflow.

+
+
+ +
+

13Checklists

+

Before play

+
    +
  • systemctl is-active gtnh-server.service returns active.
  • +
  • ss -ltnup | grep 25565 shows a listener.
  • +
  • Client is GTNH 2.8.4 Java 17-25.
  • +
  • Join as wagoqi or another whitelisted player.
  • +
+ +

Before risky work

+
    +
  • Run sudo systemctl start gtnh-backup.service.
  • +
  • Confirm a fresh archive appears in /srv/gtnh-backups.
  • +
  • Stop the server cleanly.
  • +
  • Move directories aside; do not delete them until validation is complete.
  • +
+ +

After a restore or update

+
    +
  • Server reaches the ready line in logs.
  • +
  • Inventory and position are correct.
  • +
  • Quest book progress is correct.
  • +
  • Machines, multiblocks, and dimensions are present.
  • +
  • Run save-all after confirming the world is good.
  • +
+
+
+
+ +
+ Nemesis GTNH guide. Server: GT New Horizons 2.8.4, Java 25, NixOS systemd, world Ad Infinitum. +
+
+ + diff --git a/docs/gtnh-server-migration.md b/docs/gtnh-server-migration.md new file mode 100644 index 00000000..a6e1bbda --- /dev/null +++ b/docs/gtnh-server-migration.md @@ -0,0 +1,568 @@ +# GTNH 2.8.4 Prism-to-NixOS server migration plan + +This document captures an end-to-end implementation plan for migrating a local Prism Launcher GT New Horizons `2.8.4` singleplayer instance to a native NixOS `systemd` server on `nemesis`. + +Primary references: + +- GTNH Server Setup: +- GTNH Installing and Migrating: +- GTNH Linux/Oracle setup guide, Java 25 notes and systemd/tmux examples: +- itzg/docker-minecraft-server GTNH docs, useful for GTNH-specific defaults and Java args: +- NixOS Minecraft Server wiki, useful for NixOS service behavior/console/FIFO pattern: +- NixOS upstream module implementation for `services.minecraft-server`: `nixos/modules/services/games/minecraft-server.nix` in nixpkgs. + +## Design choice + +Use a native custom NixOS module instead of the stock `services.minecraft-server` module. + +Why: + +- GTNH `2.8.4` Java 17+ server starts with `@java9args.txt -jar lwjgl3ify-forgePatches.jar nogui`, not a normal vanilla server jar. +- The stock NixOS module is excellent for vanilla-style packages exposing `/bin/minecraft-server`, but GTNH is easier and clearer as a dedicated service. +- We still borrow the NixOS module's useful FIFO console pattern so commands can be sent with `echo "say hi" > /run/gtnh-server.stdin`. + +GTNH-specific constraints from the GTNH docs: + +- Server and client GTNH versions must match exactly. +- Use the official GTNH server pack, not a Prism client instance, as the server base. +- For GTNH `2.8+`, Java 25 is supported/recommended. +- The official server pack for this migration is: + +```text +https://downloads.gtnewhorizons.com/ServerPacks/GT_New_Horizons_2.8.4_Server_Java_17-25.zip +``` + +## 1. Add the GTNH server NixOS module + +Create `nix/modules/gtnh-server.nix`: + +```nix +{ + # Standard NixOS module arguments. + # `pkgs` gives access to Java and shell utilities. + # `lib` is included for future option work even if this first version is mostly static. + config, + lib, + pkgs, + ... +}: +let + # Persistent state directory for the GTNH server. + # This holds the unpacked official server pack, configs, world, logs, backups, etc. + # It is intentionally outside the Nix store because Minecraft server state is mutable. + gtnhDir = "/srv/gtnh"; + + # GTNH 2.8+ supports Java 17-25; GTNH docs currently recommend Java 25 for 2.8+. + # Source: GTNH Installing and Migrating + Linux/Oracle server setup guide. + java = pkgs.jdk25_headless; + + # Graceful shutdown helper. + # We send `stop` to the server console FIFO first, mirroring the console/FIFO approach + # used by NixOS's stock minecraft-server module. If the process does not exit in time, + # systemd can still terminate it. + stopScript = pkgs.writeShellScript "gtnh-stop" '' + set -euo pipefail + + # Ask Minecraft/Forge to save and stop cleanly. + if [ -p /run/gtnh-server.stdin ]; then + echo "stop" > /run/gtnh-server.stdin + fi + + # Wait up to 120 seconds because GTNH can take a while to save large worlds. + timeout=120 + while kill -0 "$1" 2>/dev/null && [ "$timeout" -gt 0 ]; do + sleep 1 + timeout=$((timeout - 1)) + done + + # If the JVM is still alive after the graceful timeout, ask it to terminate. + if kill -0 "$1" 2>/dev/null; then + kill -TERM "$1" + fi + ''; +in +{ + config.flake.modules.nixos.gtnh-server = { + # Dedicated unprivileged service account. + # Minecraft does not need to run as a login user or as root. + users.users.gtnh = { + description = "GT New Horizons server user"; + isSystemUser = true; + group = "gtnh"; + home = gtnhDir; + createHome = true; + }; + + users.groups.gtnh = { }; + + # Ensure /srv/gtnh exists with the correct owner and restrictive permissions. + # The actual server pack and world are installed/copied there outside the Nix store. + systemd.tmpfiles.rules = [ + "d ${gtnhDir} 0750 gtnh gtnh -" + ]; + + # Open default Minecraft Java Edition port. + # GTNH docs and Minecraft defaults use 25565. + # TCP is gameplay; UDP is commonly opened for pings/query compatibility. + networking.firewall.allowedTCPPorts = [ 25565 ]; + networking.firewall.allowedUDPPorts = [ 25565 ]; + + # FIFO socket for server console input. + # This gives a simple admin path: + # echo "say hello" | sudo tee /run/gtnh-server.stdin + # Pattern reference: NixOS `services.minecraft-server` module and wiki. + systemd.sockets.gtnh-server = { + bindsTo = [ "gtnh-server.service" ]; + socketConfig = { + ListenFIFO = "/run/gtnh-server.stdin"; + SocketMode = "0660"; + SocketUser = "gtnh"; + SocketGroup = "gtnh"; + RemoveOnStop = true; + FlushPending = true; + }; + }; + + systemd.services.gtnh-server = { + description = "GT New Horizons 2.8.4 Server"; + wantedBy = [ "multi-user.target" ]; + requires = [ "gtnh-server.socket" ]; + after = [ + "network-online.target" + "gtnh-server.socket" + ]; + wants = [ "network-online.target" ]; + + # Put Java and basic shell utilities in PATH for any scripts or diagnostics. + path = [ + java + pkgs.coreutils + pkgs.bash + ]; + + serviceConfig = { + User = "gtnh"; + Group = "gtnh"; + WorkingDirectory = gtnhDir; + + # Restart on JVM crashes, but not after a clean `stop`. + Restart = "on-failure"; + RestartSec = "30s"; + SuccessExitStatus = "0 143"; + + # Connect systemd's FIFO socket to stdin so console commands can be sent. + StandardInput = "socket"; + StandardOutput = "journal"; + StandardError = "journal"; + + # GTNH Java 17+ startup form. + # Sources: + # - GTNH Java 17+ server pack startserver-java9.sh + # - GTNH Linux/Oracle guide suggests Java 25 + ZGC + # - itzg GTNH docs list: -Dfml.readTimeout=180 @java9args.txt -jar lwjgl3ify-forgePatches.jar + ExecStart = '' + ${java}/bin/java \ + -Xms6G \ + -Xmx10G \ + -XX:+UseZGC \ + -Dfml.readTimeout=180 \ + @java9args.txt \ + -jar lwjgl3ify-forgePatches.jar \ + nogui + ''; + + # Use the graceful stop script above. + ExecStop = "${stopScript} $MAINPID"; + + # Basic hardening. Keep it moderate because modded Minecraft writes many files + # under its working directory and may behave poorly with very strict sandboxing. + UMask = "0027"; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ReadWritePaths = [ gtnhDir ]; + ProtectHome = true; + }; + }; + }; +} +``` + +## 2. Import the server module on `nemesis` + +Edit `nix/hosts.nix` and add `cfg.modules.nixos.gtnh-server` to `nemesis.modules`: + +```nix +modules = [ + cfg.modules.nixos.user-primary + cfg.modules.nixos.nvidia-graphics + cfg.modules.nixos.steam + cfg.modules.nixos.prismlauncher + cfg.modules.nixos.daily-midnight-poweroff + + # Enables the native GTNH 2.8.4 systemd service on nemesis. + # Defined in nix/modules/gtnh-server.nix. + cfg.modules.nixos.gtnh-server + + ( + { + config, + pkgs, + lib, + modulesPath, + ... + }: + { + # Existing nemesis hardware/filesystem/boot config remains here. + } + ) +]; +``` + +## 3. Check and rebuild NixOS + +Run from the repo on `nemesis`: + +```sh +# Track the new module before evaluation. New Nix files must be git-tracked +# for flake evaluation in this repo workflow. +git add nix/modules/gtnh-server.nix nix/hosts.nix + +# Format only Nix files using the repo's just recipe. +nix develop -c just format-nix + +# Validate Nix configuration. +nix develop -c just check-nix + +# Inspect generated changes before rebuild. +git diff + +# Rebuild the current host. Repo policy: only do this after explicit approval. +nix develop -c just rb +``` + +## 4. Install official GTNH server files + +The service will not start successfully until the official server pack has been unpacked into `/srv/gtnh`. + +```sh +# Stop the service in case systemd attempted to start it before files existed. +sudo systemctl stop gtnh-server.service + +# Download the official GTNH 2.8.4 Java 17-25 server pack. +# Source: GTNH downloads/server setup docs. +cd /tmp +curl -LO https://downloads.gtnewhorizons.com/ServerPacks/GT_New_Horizons_2.8.4_Server_Java_17-25.zip + +# Ensure target state directory exists. +sudo mkdir -p /srv/gtnh + +# Unpack into /srv/gtnh. Use bsdtar if present. +sudo bsdtar -xf GT_New_Horizons_2.8.4_Server_Java_17-25.zip -C /srv/gtnh + +# If bsdtar is unavailable, use comma to run unzip without permanently installing it: +# , unzip GT_New_Horizons_2.8.4_Server_Java_17-25.zip -d /tmp/gtnh-unpack +# sudo rsync -a /tmp/gtnh-unpack/ /srv/gtnh/ + +# Accept Mojang's EULA. Required by both GTNH/NixOS docs and Minecraft itself. +echo "eula=true" | sudo tee /srv/gtnh/eula.txt + +# Make all server files owned by the service account. +sudo chown -R gtnh:gtnh /srv/gtnh +sudo chmod -R u+rwX,g+rX,o-rwx /srv/gtnh + +# Sanity-check that Java 17+ GTNH startup files are present. +ls -la /srv/gtnh/lwjgl3ify-forgePatches.jar /srv/gtnh/java9args.txt +``` + +## 5. Configure `server.properties` + +Edit `/srv/gtnh/server.properties`: + +```sh +sudoedit /srv/gtnh/server.properties +``` + +Recommended baseline: + +```properties +# World folder name under /srv/gtnh. +# The migration below copies the Prism world into /srv/gtnh/world. +level-name=world + +# GTNH intended world generation. +# Sources: GTNH Server Setup FAQ and itzg GTNH defaults. +level-type=rwg + +# GTNH default/recommended difficulty. +difficulty=hard + +# Required/recommended for modded movement and GTNH server defaults. +allow-flight=true + +# GTNH/itzg default. Keep enabled unless there is a reason not to. +enable-command-block=true + +# Private local server safety. Add players with `whitelist add USERNAME`. +white-list=true + +# Keep small for a local/private server. +max-players=5 + +# Default Minecraft port; firewall module opens this. +server-port=25565 + +# Server list display text. +motd=GT New Horizons 2.8.4 on nemesis +``` + +Then fix ownership if needed: + +```sh +sudo chown gtnh:gtnh /srv/gtnh/server.properties +``` + +## 6. First boot test with a generated empty world + +Do this before copying the Prism world. It proves the server pack, Java, service, firewall, and console FIFO work. + +```sh +# Start GTNH. First launch can take several minutes. +sudo systemctl start gtnh-server.service + +# Follow logs. Wait until startup completes. +journalctl -fu gtnh-server.service + +# In another terminal, test console input through the FIFO. +echo "say GTNH server online" | sudo tee /run/gtnh-server.stdin + +# Stop cleanly before migration. +echo "stop" | sudo tee /run/gtnh-server.stdin + +# Confirm stopped or stopping cleanly. +systemctl status gtnh-server.service +``` + +## 7. Locate and back up the Prism singleplayer world + +NOTE: backups must include other folders than **/saves. check the gtnh wiki and local instance backup configuration. + +Prism instances are usually under `~/.local/share/PrismLauncher/instances` on Linux. + +```sh +# Find Prism saves directories. +find ~/.local/share/PrismLauncher/instances -maxdepth 3 -type d -name saves +``` + +Typical world path: + +```text +~/.local/share/PrismLauncher/instances//.minecraft/saves/ +``` + +Backup the world before touching it: + +```sh +# Replace and . +mkdir -p ~/gtnh-migration-backups + +tar -C ~/.local/share/PrismLauncher/instances//.minecraft/saves \ + -czf ~/gtnh-migration-backups/-before-server-migration.tar.gz \ + +``` + +## 8. Copy the Prism world to the server + +```sh +# Server must be stopped before replacing the world. +sudo systemctl stop gtnh-server.service + +# Move the generated test world aside instead of deleting it. +sudo mv /srv/gtnh/world /srv/gtnh/world.empty-test.$(date +%Y%m%d-%H%M%S) + +# Copy the Prism singleplayer save as the server's `world` directory. +# The trailing slashes are intentional: copy contents of into /srv/gtnh/world. +sudo rsync -a --info=progress2 \ + ~/.local/share/PrismLauncher/instances//.minecraft/saves// \ + /srv/gtnh/world/ + +# Give ownership to the service account. +sudo chown -R gtnh:gtnh /srv/gtnh/world + +# Start the migrated world. +sudo systemctl start gtnh-server.service +journalctl -fu gtnh-server.service +``` + +## 9. Whitelist and op yourself + +Run after the server is up: + +```sh +# Replace YOUR_USERNAME with the exact Minecraft username. +# Whitelist is recommended for private/self-hosted servers. +echo "whitelist add YOUR_USERNAME" | sudo tee /run/gtnh-server.stdin + +# Give yourself admin permissions for migration validation and future admin tasks. +echo "op YOUR_USERNAME" | sudo tee /run/gtnh-server.stdin + +# Force a save after changing admin files. +echo "save-all" | sudo tee /run/gtnh-server.stdin +``` + +Connect from Prism: + +```text +nemesis:25565 +``` + +or use the LAN IP of `nemesis`. + +## 10. Validate the migrated world + +Verify before deleting any backups: + +- Spawn location is expected. +- Inventory and armor are correct. +- Quest book progress is intact. +- Machines/multiblocks are present and valid. +- Dimensions are present. +- Claimed/chunk-loaded areas behave as expected. +- Server TPS is acceptable after initial chunk loading. + +Caveat: + +- Singleplayer-to-server migrations can sometimes expose Minecraft `level.dat` player data vs UUID-based `playerdata` differences. If inventory or position is wrong, stop the server and keep the original save backup. Fixing is possible with NBT tooling, but should be done carefully. + +## 11. Optional: add system-level GTNH backups + +GTNH's ServerUtilities includes backups, but an external host-level backup is still useful. + +Create `nix/modules/gtnh-backups.nix`: + +```nix +{ pkgs, ... }: + +let + # Backup destination outside the live server directory. + backupDir = "/srv/gtnh-backups"; +in +{ + config.flake.modules.nixos.gtnh-backups = { + # Ensure backup directory exists. Owned by root because backups are written by systemd. + systemd.tmpfiles.rules = [ + "d ${backupDir} 0750 root root -" + ]; + + systemd.services.gtnh-backup = { + description = "Backup GTNH server"; + serviceConfig = { + Type = "oneshot"; + + ExecStart = pkgs.writeShellScript "gtnh-backup" '' + set -euo pipefail + + ts="$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S)" + dest="${backupDir}/gtnh-$ts.tar.zst" + + # Ask the server to flush world data before taking a filesystem backup. + # This is not as strong as a fully quiesced backup, but is simple and low disruption. + echo "say Starting server backup" > /run/gtnh-server.stdin || true + echo "save-all" > /run/gtnh-server.stdin || true + sleep 10 + + # Archive /srv/gtnh as mutable state. + # Exclude GTNH's own backup folder to avoid recursive backup bloat. + ${pkgs.gnutar}/bin/tar \ + --exclude='/srv/gtnh/backups' \ + -C /srv \ + -I '${pkgs.zstd}/bin/zstd -T0 -10' \ + -cf "$dest" \ + gtnh + + # Keep 14 days of host-level backups. + ${pkgs.findutils}/bin/find ${backupDir} \ + -name 'gtnh-*.tar.zst' \ + -type f \ + -mtime +14 \ + -delete + + echo "say Server backup complete" > /run/gtnh-server.stdin || true + ''; + }; + }; + + systemd.timers.gtnh-backup = { + wantedBy = [ "timers.target" ]; + timerConfig = { + # Run daily at 03:30 local time. + OnCalendar = "03:30"; + + # If the machine was off at 03:30, run once after boot. + Persistent = true; + }; + }; + }; +} +``` + +Import it in `nemesis.modules`: + +```nix +modules = [ + # Existing nemesis modules... + cfg.modules.nixos.gtnh-server + + # Adds a daily compressed /srv/gtnh backup timer. + cfg.modules.nixos.gtnh-backups +]; +``` + +Apply: + +```sh +git add nix/modules/gtnh-backups.nix nix/hosts.nix +nix develop -c just format-nix +nix develop -c just check-nix +nix develop -c just rb +``` + +Manual backup test: + +```sh +sudo systemctl start gtnh-backup.service +ls -lh /srv/gtnh-backups +``` + +## 12. Admin command reference + +```sh +# Follow logs. +journalctl -fu gtnh-server.service + +# Service lifecycle. +sudo systemctl start gtnh-server.service +sudo systemctl stop gtnh-server.service +sudo systemctl restart gtnh-server.service +sudo systemctl status gtnh-server.service + +# Send Minecraft console commands through the FIFO. +echo "say hello" | sudo tee /run/gtnh-server.stdin +echo "list" | sudo tee /run/gtnh-server.stdin +echo "save-all" | sudo tee /run/gtnh-server.stdin +echo "stop" | sudo tee /run/gtnh-server.stdin + +# Check listener. +ss -ltnup | grep 25565 + +# Check backup timer. +systemctl list-timers | grep gtnh +``` + +## 13. Commit + +```sh +git status +git add nix/modules/gtnh-server.nix nix/modules/gtnh-backups.nix nix/hosts.nix docs/gtnh-server-migration.md +git commit -m "feat(gtnh-server): document nemesis GTNH migration" +``` diff --git a/nix/hosts.nix b/nix/hosts.nix index 5c9d851e..b245074d 100644 --- a/nix/hosts.nix +++ b/nix/hosts.nix @@ -61,6 +61,8 @@ in cfg.modules.nixos.steam cfg.modules.nixos.prismlauncher cfg.modules.nixos.daily-midnight-poweroff + cfg.modules.nixos.gtnh-server + cfg.modules.nixos.gtnh-backups ( { config, diff --git a/nix/modules/gtnh-backups.nix b/nix/modules/gtnh-backups.nix new file mode 100644 index 00000000..d18946d3 --- /dev/null +++ b/nix/modules/gtnh-backups.nix @@ -0,0 +1,50 @@ +{ + config.flake.modules.nixos.gtnh-backups = + { pkgs, ... }: + let + backupDir = "/srv/gtnh-backups"; + in + { + systemd = { + tmpfiles.rules = [ "d ${backupDir} 0750 root root -" ]; + services.gtnh-backup = { + description = "Backup GTNH server"; + serviceConfig = { + Type = "oneshot"; + ExecStart = pkgs.writeShellScript "gtnh-backup" '' + set -euo pipefail + + ts="$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S)" + dest="${backupDir}/gtnh-$ts.tar.zst" + + echo "say Starting server backup" > /run/gtnh-server.stdin || true + echo "save-all" > /run/gtnh-server.stdin || true + ${pkgs.coreutils}/bin/sleep 10 + + ${pkgs.gnutar}/bin/tar \ + --exclude='/srv/gtnh/backups' \ + -C /srv \ + -I '${pkgs.zstd}/bin/zstd -T0 -10' \ + -cf "$dest" \ + gtnh + + ${pkgs.findutils}/bin/find ${backupDir} \ + -name 'gtnh-*.tar.zst' \ + -type f \ + -mtime +14 \ + -delete + + echo "say Server backup complete" > /run/gtnh-server.stdin || true + ''; + }; + }; + timers.gtnh-backup = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "03:30"; + Persistent = true; + }; + }; + }; + }; +} diff --git a/nix/modules/gtnh-server.nix b/nix/modules/gtnh-server.nix new file mode 100644 index 00000000..b53f7372 --- /dev/null +++ b/nix/modules/gtnh-server.nix @@ -0,0 +1,102 @@ +{ + config.flake.modules.nixos.gtnh-server = + { pkgs, ... }: + let + gtnhDir = "/srv/gtnh"; + java = pkgs.jdk25_headless; + stopScript = pkgs.writeShellScript "gtnh-stop" '' + set -euo pipefail + + if [ -p /run/gtnh-server.stdin ]; then + echo "stop" > /run/gtnh-server.stdin || true + fi + + timeout=120 + while kill -0 "$1" 2>/dev/null && [ "$timeout" -gt 0 ]; do + sleep 1 + timeout=$((timeout - 1)) + done + + if kill -0 "$1" 2>/dev/null; then + kill -TERM "$1" + fi + ''; + in + { + users = { + users.gtnh = { + description = "GT New Horizons server user"; + isSystemUser = true; + group = "gtnh"; + home = gtnhDir; + createHome = true; + }; + groups.gtnh = { }; + }; + systemd = { + tmpfiles.rules = [ "d ${gtnhDir} 0750 gtnh gtnh -" ]; + sockets.gtnh-server = { + bindsTo = [ "gtnh-server.service" ]; + socketConfig = { + ListenFIFO = "/run/gtnh-server.stdin"; + SocketMode = "0660"; + SocketUser = "gtnh"; + SocketGroup = "gtnh"; + RemoveOnStop = true; + FlushPending = true; + }; + }; + services.gtnh-server = { + description = "GT New Horizons 2.8.4 Server"; + wantedBy = [ "multi-user.target" ]; + requires = [ "gtnh-server.socket" ]; + after = [ + "network-online.target" + "gtnh-server.socket" + ]; + wants = [ "network-online.target" ]; + unitConfig.ConditionPathExists = [ + "${gtnhDir}/java9args.txt" + "${gtnhDir}/lwjgl3ify-forgePatches.jar" + ]; + path = [ + java + pkgs.bash + pkgs.coreutils + ]; + serviceConfig = { + User = "gtnh"; + Group = "gtnh"; + WorkingDirectory = gtnhDir; + Restart = "on-failure"; + RestartSec = "30s"; + SuccessExitStatus = "0 143"; + StandardInput = "socket"; + StandardOutput = "journal"; + StandardError = "journal"; + ExecStart = '' + ${java}/bin/java \ + -Xms6G \ + -Xmx10G \ + -XX:+UseZGC \ + -Dfml.readTimeout=180 \ + @java9args.txt \ + -jar lwjgl3ify-forgePatches.jar \ + nogui + ''; + ExecStop = "${stopScript} $MAINPID"; + UMask = "0027"; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ReadWritePaths = [ gtnhDir ]; + ProtectHome = true; + }; + }; + }; + networking.firewall = { + allowedTCPPorts = [ 25565 ]; + allowedUDPPorts = [ 25565 ]; + }; + }; +} From 18e3d61c20ea8f041b2dbf5f0fa69cfc1f9071c9 Mon Sep 17 00:00:00 2001 From: Mohammad Rafiq Date: Mon, 8 Jun 2026 15:51:13 +0800 Subject: [PATCH 3/3] chore: remove docs --- docs/gtnh-server-guide.html | 652 ---------------------------------- docs/gtnh-server-migration.md | 568 ----------------------------- 2 files changed, 1220 deletions(-) delete mode 100644 docs/gtnh-server-guide.html delete mode 100644 docs/gtnh-server-migration.md diff --git a/docs/gtnh-server-guide.html b/docs/gtnh-server-guide.html deleted file mode 100644 index d274f5e0..00000000 --- a/docs/gtnh-server-guide.html +++ /dev/null @@ -1,652 +0,0 @@ - - - - - - Nemesis GTNH Server Guide - - - -
-
-
nemesis is serving GT New Horizons
-

GTNH 2.8.4 Server Guide

-

This is the operating manual for the GT New Horizons server running on nemesis. It covers connecting from Prism, day-to-day administration, backups, restores, upgrades, and the parts of the NixOS setup that keep the server reproducible.

-
-
Connect address
nemesis:25565
-
LAN IP
10.10.0.11:25565
-
Tailscale IP
100.98.114.23:25565
-
World
Ad Infinitum
-
-
- -
- - -
-
-

1Quick start

-

The server is already installed, rebuilt into the nemesis NixOS configuration, started, listening on port 25565, and running the migrated Ad Infinitum world.

-
-
-

Join from Prism

-
    -
  1. Launch GT_New_Horizons_2.8.4_Java_17-25.
  2. -
  3. Open Multiplayer.
  4. -
  5. Add server nemesis:25565. If local DNS is not available, use 10.10.0.11:25565.
  6. -
  7. Join as wagoqi.
  8. -
-
-
-

Current access

-
    -
  • Whitelist is enabled.
  • -
  • wagoqi is whitelisted.
  • -
  • wagoqi is an operator at permission level 2.
  • -
  • Online-mode is enabled.
  • -
-
-
-
-

Client version must match exactly. Use GTNH 2.8.4, Java 17-25 client. A different GTNH version can fail during handshake or, worse, expose mismatched configs and scripts.

-
-
- -
-

2What is running

-

The server is a native NixOS systemd service, not a Docker container and not a Prism-launched client instance. The mutable Minecraft state lives in /srv/gtnh; the service definition lives in this repo.

- - - - - - - - - - - - - -
ItemValue
Hostnemesis
Servicegtnh-server.service
Console FIFO/run/gtnh-server.stdin
Server directory/srv/gtnh
World directory/srv/gtnh/world
Official server packGT_New_Horizons_2.8.4_Server_Java_17-25.zip
JavaNixOS jdk25_headless
Memory flags-Xms6G -Xmx10G -XX:+UseZGC
Startup jarlwjgl3ify-forgePatches.jar with @java9args.txt
-
- TCP 25565 open - UDP 25565 open - daily host backup - systemd restart on failure -
-
- -
-

3Client setup

-

Use the existing Prism instance

-

The migrated world came from this Prism instance:

-
~/.local/share/PrismLauncher/instances/GT_New_Horizons_2.8.4_Java_17-25
-

Use that exact instance when connecting. The server uses the official 2.8.4 Java 17-25 server pack, which is the matching server-side distribution for that client.

- -

Addresses to try

- - - - - - - -
Where you areAddressNotes
On the same LANnemesis:25565Preferred if hostname resolution works.
On the same LAN, no hostname10.10.0.11:25565Direct wired LAN address.
Over Tailscale100.98.114.23:25565Use when away from the LAN and Tailscale is connected.
- -

First login validation

-
    -
  • Inventory, armor, Baubles, Thaumcraft data, and location should match the singleplayer save.
  • -
  • Quest progress should be present. The server log loaded BetterQuesting progress for two UUIDs.
  • -
  • Machines and multiblocks should be in place.
  • -
  • Dimensions should exist. The server loaded many Galacticraft and modded dimensions during first boot.
  • -
-
-

If inventory or position is wrong, stop immediately. Do not keep playing and saving. Singleplayer-to-server migrations can expose UUID/playerdata differences. The original save backup exists so playerdata can be repaired carefully.

-
-
- -
-

4Server controls

-

Use systemd for process lifecycle. Use the console FIFO for in-game commands. Do not kill the Java process directly unless the service is stuck and a normal stop has failed.

-
-
sudo systemctl status gtnh-server.serviceShow service state, main PID, memory use, recent logs.
-
journalctl -fu gtnh-server.serviceFollow live server logs.
-
sudo systemctl start gtnh-server.serviceStart the server.
-
sudo systemctl stop gtnh-server.serviceGracefully stops by sending stop through the server console.
-
sudo systemctl restart gtnh-server.serviceGraceful stop followed by start. Use after config changes.
-
ss -ltnup | grep 25565Confirm the Minecraft listener is present.
-
-
-

The service has startup conditions for /srv/gtnh/java9args.txt and /srv/gtnh/lwjgl3ify-forgePatches.jar. If those files are missing, systemd skips the service instead of crash-looping a half-installed server.

-
-
- -
-

5Console commands

-

Systemd owns a FIFO at /run/gtnh-server.stdin. Writing a line to it is the same as typing into the dedicated server console.

-
echo "say hello from nemesis" | sudo tee /run/gtnh-server.stdin
- - - - - - - - - - -
CommandPurpose
echo "list" | sudo tee /run/gtnh-server.stdinList online players.
echo "say Server restart in 5 minutes" | sudo tee /run/gtnh-server.stdinBroadcast a message.
echo "save-all" | sudo tee /run/gtnh-server.stdinForce a world save.
echo "stop" | sudo tee /run/gtnh-server.stdinAsk Minecraft/Forge to stop cleanly.
echo "whitelist add USER" | sudo tee /run/gtnh-server.stdinAllow a player to join.
echo "op USER" | sudo tee /run/gtnh-server.stdinGrant operator privileges.
-
-

Prefer systemctl stop for routine shutdowns. The NixOS service has a graceful stop script that sends stop and waits up to 120 seconds before sending TERM.

-
-
- -
-

6Players, whitelist, and ops

-

The server is private. white-list=true and online-mode=true are set in /srv/gtnh/server.properties.

-

Add a player

-
echo "whitelist add PLAYERNAME" | sudo tee /run/gtnh-server.stdin
-

Remove a player

-
echo "whitelist remove PLAYERNAME" | sudo tee /run/gtnh-server.stdin
-

Give or remove operator access

-
echo "op PLAYERNAME" | sudo tee /run/gtnh-server.stdin
-echo "deop PLAYERNAME" | sudo tee /run/gtnh-server.stdin
-

Current admin

- - - - - -
PlayerUUIDState
wagoqi22c6c7d9-3c10-41a0-9e9d-759f2aeec75bWhitelisted and op level 2.
-
- -
-

7World and state layout

-

The mutable state is intentionally outside the Nix store. The NixOS module defines the service; /srv/gtnh holds the actual server files, world, logs, backups, and generated configs.

- - - - - - - - - - - -
PathWhat it is
/srv/gtnhServer root and working directory.
/srv/gtnh/worldThe migrated Ad Infinitum save, renamed to the server's level-name=world.
/srv/gtnh/server.propertiesMinecraft server settings. Keep level-name=world.
/srv/gtnh/logsGTNH/Forge log files in addition to journald.
/srv/gtnh/backupsServerUtilities' in-pack backups.
/srv/gtnh-backupsHost-level compressed backups produced by the NixOS timer.
~/gtnh-migration-backupsOriginal pre-migration save backup from the Prism world.
- -

Important server.properties values

-
level-name=world
-level-type=rwg
-difficulty=3
-allow-flight=true
-enable-command-block=true
-white-list=true
-max-players=5
-server-port=25565
-motd=GT New Horizons 2.8.4 on nemesis
-
-

Do not rename /srv/gtnh/world or change level-name casually. If they stop matching, the server will create or load a different world.

-
-
- -
-

8Backups

-

There are three useful backup layers. Keep all of them until the migrated world has been played and validated for a while.

-
-
-

Migration backup

-

Created before copying Ad Infinitum to the server.

-
~/gtnh-migration-backups/Ad-Infinitum-before-server-migration-*.tar.gz
-
-
-

GTNH ServerUtilities

-

In-pack backups written below the live server directory.

-
/srv/gtnh/backups
-
-
-

Host-level daily backup

-

NixOS timer archives /srv/gtnh to zstd-compressed tarballs.

-
/srv/gtnh-backups/gtnh-YYYYMMDD-HHMMSS.tar.zst
-
-
-

Manual copy before risky work

-

Before updates, config surgery, or NBT edits, take a named manual archive.

-
sudo systemctl start gtnh-backup.service
-
-
- -

Check the timer

-
systemctl list-timers | grep gtnh
-sudo ls -lh /srv/gtnh-backups
- -

Run a manual backup

-
sudo systemctl start gtnh-backup.service
-journalctl -u gtnh-backup.service -n 80 --no-pager
-
-

The host backup asks the server to save-all, waits 10 seconds, then archives /srv/gtnh. It excludes /srv/gtnh/backups to avoid backup recursion.

-
-
- -
-

9Restore procedure

-

Restores should be boring and deliberate. Stop the server, move the damaged state aside, unpack the backup, fix ownership, and start again.

-
    -
  1. Announce downtime if players are online.
  2. -
  3. Stop the server cleanly.
  4. -
  5. Keep the current broken state instead of deleting it.
  6. -
  7. Unpack a known-good backup.
  8. -
  9. Restore ownership and permissions.
  10. -
  11. Start the server and watch logs until it reaches ready state.
  12. -
-
echo "say Server restore starting" | sudo tee /run/gtnh-server.stdin
-sudo systemctl stop gtnh-server.service
-sudo mv /srv/gtnh /srv/gtnh.broken.$(date +%Y%m%d-%H%M%S)
-sudo mkdir -p /srv/gtnh
-sudo tar -C /srv -I 'zstd -d' -xf /srv/gtnh-backups/gtnh-YYYYMMDD-HHMMSS.tar.zst
-sudo chown -R gtnh:gtnh /srv/gtnh
-sudo chmod -R u+rwX,g+rX,o-rwx /srv/gtnh
-sudo systemctl start gtnh-server.service
-journalctl -fu gtnh-server.service
-
-

Replace gtnh-YYYYMMDD-HHMMSS.tar.zst with the actual backup file. If zstd is missing in an ad-hoc shell, enter the repo dev shell or run through comma.

-
-
- -
-

10Updating GTNH

-

Do not update casually. GTNH server and client versions must match exactly. The current installation is pinned operationally to GTNH 2.8.4 using the official Java 17-25 server pack.

-

Safe update shape

-
    -
  1. Read the GTNH migration notes for the target version.
  2. -
  3. Update the Prism client first in a copy of the instance, not the only playable instance.
  4. -
  5. Run sudo systemctl start gtnh-backup.service.
  6. -
  7. Stop the server.
  8. -
  9. Unpack the new official server pack into a staging directory.
  10. -
  11. Copy or merge only the state that belongs across versions, following GTNH docs.
  12. -
  13. Start and validate before deleting any old pack or backup.
  14. -
-
-

Never replace /srv/gtnh with a Prism client instance. Use the official server pack for the target version and migrate the world/state into it.

-
-
- -
-

11Troubleshooting

-

Server does not show as online

-
systemctl status gtnh-server.service --no-pager -l
-ss -ltnup | grep 25565
-journalctl -u gtnh-server.service -n 160 --no-pager
-
    -
  • If the service is inactive with unmet conditions, check that /srv/gtnh/java9args.txt and /srv/gtnh/lwjgl3ify-forgePatches.jar exist.
  • -
  • If it is starting, wait. GTNH first boot can take several minutes.
  • -
  • If it crashed, inspect the journal and /srv/gtnh/crash-reports.
  • -
- -

Client says mod mismatch

-

Use the exact GTNH 2.8.4 Java 17-25 Prism instance. Do not connect from a different GTNH version or a local instance with hand-edited mods/configs.

- -

Whitelist rejection

-
echo "whitelist list" | sudo tee /run/gtnh-server.stdin
-echo "whitelist add PLAYERNAME" | sudo tee /run/gtnh-server.stdin
- -

Wrong inventory or spawn after migration

-
    -
  1. Stop the server before more saves happen.
  2. -
  3. Keep the broken state for inspection.
  4. -
  5. Compare level.dat player data and world/playerdata/*.dat UUID data against the original Prism save.
  6. -
  7. Use an NBT editor carefully; do not improvise on the only copy.
  8. -
- -

Lag after startup

-

Some initial lag is expected while GTNH loads dimensions and caches. If TPS remains bad, check memory and logs:

-
systemctl status gtnh-server.service --no-pager -l
-journalctl -u gtnh-server.service -n 200 --no-pager | grep -iE 'tps|tick|overload|warn|error'
-
- -
-

12NixOS implementation

-

The NixOS pieces are in the repo so the service survives rebuilds and host reconfiguration. The Minecraft world remains mutable data under /srv/gtnh.

- - - - - - - - -
Repo fileRole
nix/modules/gtnh-server.nixDefines the gtnh user/group, firewall ports, FIFO socket, Java command, service hardening, and graceful stop behavior.
nix/modules/gtnh-backups.nixDefines the daily backup service and timer.
nix/hosts.nixImports the GTNH server and backup modules on nemesis.
docs/gtnh-server-migration.mdOriginal migration plan and references.
- -

Validate Nix changes

-
nix develop -c just format-nix
-nix develop -c just check-nix
-nix develop -c just rb
-
-

Rebuilds are host actions. Run just rb on nemesis for this server. New Nix files must be tracked by git before flake evaluation in this repo workflow.

-
-
- -
-

13Checklists

-

Before play

-
    -
  • systemctl is-active gtnh-server.service returns active.
  • -
  • ss -ltnup | grep 25565 shows a listener.
  • -
  • Client is GTNH 2.8.4 Java 17-25.
  • -
  • Join as wagoqi or another whitelisted player.
  • -
- -

Before risky work

-
    -
  • Run sudo systemctl start gtnh-backup.service.
  • -
  • Confirm a fresh archive appears in /srv/gtnh-backups.
  • -
  • Stop the server cleanly.
  • -
  • Move directories aside; do not delete them until validation is complete.
  • -
- -

After a restore or update

-
    -
  • Server reaches the ready line in logs.
  • -
  • Inventory and position are correct.
  • -
  • Quest book progress is correct.
  • -
  • Machines, multiblocks, and dimensions are present.
  • -
  • Run save-all after confirming the world is good.
  • -
-
-
-
- -
- Nemesis GTNH guide. Server: GT New Horizons 2.8.4, Java 25, NixOS systemd, world Ad Infinitum. -
-
- - diff --git a/docs/gtnh-server-migration.md b/docs/gtnh-server-migration.md deleted file mode 100644 index a6e1bbda..00000000 --- a/docs/gtnh-server-migration.md +++ /dev/null @@ -1,568 +0,0 @@ -# GTNH 2.8.4 Prism-to-NixOS server migration plan - -This document captures an end-to-end implementation plan for migrating a local Prism Launcher GT New Horizons `2.8.4` singleplayer instance to a native NixOS `systemd` server on `nemesis`. - -Primary references: - -- GTNH Server Setup: -- GTNH Installing and Migrating: -- GTNH Linux/Oracle setup guide, Java 25 notes and systemd/tmux examples: -- itzg/docker-minecraft-server GTNH docs, useful for GTNH-specific defaults and Java args: -- NixOS Minecraft Server wiki, useful for NixOS service behavior/console/FIFO pattern: -- NixOS upstream module implementation for `services.minecraft-server`: `nixos/modules/services/games/minecraft-server.nix` in nixpkgs. - -## Design choice - -Use a native custom NixOS module instead of the stock `services.minecraft-server` module. - -Why: - -- GTNH `2.8.4` Java 17+ server starts with `@java9args.txt -jar lwjgl3ify-forgePatches.jar nogui`, not a normal vanilla server jar. -- The stock NixOS module is excellent for vanilla-style packages exposing `/bin/minecraft-server`, but GTNH is easier and clearer as a dedicated service. -- We still borrow the NixOS module's useful FIFO console pattern so commands can be sent with `echo "say hi" > /run/gtnh-server.stdin`. - -GTNH-specific constraints from the GTNH docs: - -- Server and client GTNH versions must match exactly. -- Use the official GTNH server pack, not a Prism client instance, as the server base. -- For GTNH `2.8+`, Java 25 is supported/recommended. -- The official server pack for this migration is: - -```text -https://downloads.gtnewhorizons.com/ServerPacks/GT_New_Horizons_2.8.4_Server_Java_17-25.zip -``` - -## 1. Add the GTNH server NixOS module - -Create `nix/modules/gtnh-server.nix`: - -```nix -{ - # Standard NixOS module arguments. - # `pkgs` gives access to Java and shell utilities. - # `lib` is included for future option work even if this first version is mostly static. - config, - lib, - pkgs, - ... -}: -let - # Persistent state directory for the GTNH server. - # This holds the unpacked official server pack, configs, world, logs, backups, etc. - # It is intentionally outside the Nix store because Minecraft server state is mutable. - gtnhDir = "/srv/gtnh"; - - # GTNH 2.8+ supports Java 17-25; GTNH docs currently recommend Java 25 for 2.8+. - # Source: GTNH Installing and Migrating + Linux/Oracle server setup guide. - java = pkgs.jdk25_headless; - - # Graceful shutdown helper. - # We send `stop` to the server console FIFO first, mirroring the console/FIFO approach - # used by NixOS's stock minecraft-server module. If the process does not exit in time, - # systemd can still terminate it. - stopScript = pkgs.writeShellScript "gtnh-stop" '' - set -euo pipefail - - # Ask Minecraft/Forge to save and stop cleanly. - if [ -p /run/gtnh-server.stdin ]; then - echo "stop" > /run/gtnh-server.stdin - fi - - # Wait up to 120 seconds because GTNH can take a while to save large worlds. - timeout=120 - while kill -0 "$1" 2>/dev/null && [ "$timeout" -gt 0 ]; do - sleep 1 - timeout=$((timeout - 1)) - done - - # If the JVM is still alive after the graceful timeout, ask it to terminate. - if kill -0 "$1" 2>/dev/null; then - kill -TERM "$1" - fi - ''; -in -{ - config.flake.modules.nixos.gtnh-server = { - # Dedicated unprivileged service account. - # Minecraft does not need to run as a login user or as root. - users.users.gtnh = { - description = "GT New Horizons server user"; - isSystemUser = true; - group = "gtnh"; - home = gtnhDir; - createHome = true; - }; - - users.groups.gtnh = { }; - - # Ensure /srv/gtnh exists with the correct owner and restrictive permissions. - # The actual server pack and world are installed/copied there outside the Nix store. - systemd.tmpfiles.rules = [ - "d ${gtnhDir} 0750 gtnh gtnh -" - ]; - - # Open default Minecraft Java Edition port. - # GTNH docs and Minecraft defaults use 25565. - # TCP is gameplay; UDP is commonly opened for pings/query compatibility. - networking.firewall.allowedTCPPorts = [ 25565 ]; - networking.firewall.allowedUDPPorts = [ 25565 ]; - - # FIFO socket for server console input. - # This gives a simple admin path: - # echo "say hello" | sudo tee /run/gtnh-server.stdin - # Pattern reference: NixOS `services.minecraft-server` module and wiki. - systemd.sockets.gtnh-server = { - bindsTo = [ "gtnh-server.service" ]; - socketConfig = { - ListenFIFO = "/run/gtnh-server.stdin"; - SocketMode = "0660"; - SocketUser = "gtnh"; - SocketGroup = "gtnh"; - RemoveOnStop = true; - FlushPending = true; - }; - }; - - systemd.services.gtnh-server = { - description = "GT New Horizons 2.8.4 Server"; - wantedBy = [ "multi-user.target" ]; - requires = [ "gtnh-server.socket" ]; - after = [ - "network-online.target" - "gtnh-server.socket" - ]; - wants = [ "network-online.target" ]; - - # Put Java and basic shell utilities in PATH for any scripts or diagnostics. - path = [ - java - pkgs.coreutils - pkgs.bash - ]; - - serviceConfig = { - User = "gtnh"; - Group = "gtnh"; - WorkingDirectory = gtnhDir; - - # Restart on JVM crashes, but not after a clean `stop`. - Restart = "on-failure"; - RestartSec = "30s"; - SuccessExitStatus = "0 143"; - - # Connect systemd's FIFO socket to stdin so console commands can be sent. - StandardInput = "socket"; - StandardOutput = "journal"; - StandardError = "journal"; - - # GTNH Java 17+ startup form. - # Sources: - # - GTNH Java 17+ server pack startserver-java9.sh - # - GTNH Linux/Oracle guide suggests Java 25 + ZGC - # - itzg GTNH docs list: -Dfml.readTimeout=180 @java9args.txt -jar lwjgl3ify-forgePatches.jar - ExecStart = '' - ${java}/bin/java \ - -Xms6G \ - -Xmx10G \ - -XX:+UseZGC \ - -Dfml.readTimeout=180 \ - @java9args.txt \ - -jar lwjgl3ify-forgePatches.jar \ - nogui - ''; - - # Use the graceful stop script above. - ExecStop = "${stopScript} $MAINPID"; - - # Basic hardening. Keep it moderate because modded Minecraft writes many files - # under its working directory and may behave poorly with very strict sandboxing. - UMask = "0027"; - NoNewPrivileges = true; - PrivateTmp = true; - ProtectSystem = "strict"; - ReadWritePaths = [ gtnhDir ]; - ProtectHome = true; - }; - }; - }; -} -``` - -## 2. Import the server module on `nemesis` - -Edit `nix/hosts.nix` and add `cfg.modules.nixos.gtnh-server` to `nemesis.modules`: - -```nix -modules = [ - cfg.modules.nixos.user-primary - cfg.modules.nixos.nvidia-graphics - cfg.modules.nixos.steam - cfg.modules.nixos.prismlauncher - cfg.modules.nixos.daily-midnight-poweroff - - # Enables the native GTNH 2.8.4 systemd service on nemesis. - # Defined in nix/modules/gtnh-server.nix. - cfg.modules.nixos.gtnh-server - - ( - { - config, - pkgs, - lib, - modulesPath, - ... - }: - { - # Existing nemesis hardware/filesystem/boot config remains here. - } - ) -]; -``` - -## 3. Check and rebuild NixOS - -Run from the repo on `nemesis`: - -```sh -# Track the new module before evaluation. New Nix files must be git-tracked -# for flake evaluation in this repo workflow. -git add nix/modules/gtnh-server.nix nix/hosts.nix - -# Format only Nix files using the repo's just recipe. -nix develop -c just format-nix - -# Validate Nix configuration. -nix develop -c just check-nix - -# Inspect generated changes before rebuild. -git diff - -# Rebuild the current host. Repo policy: only do this after explicit approval. -nix develop -c just rb -``` - -## 4. Install official GTNH server files - -The service will not start successfully until the official server pack has been unpacked into `/srv/gtnh`. - -```sh -# Stop the service in case systemd attempted to start it before files existed. -sudo systemctl stop gtnh-server.service - -# Download the official GTNH 2.8.4 Java 17-25 server pack. -# Source: GTNH downloads/server setup docs. -cd /tmp -curl -LO https://downloads.gtnewhorizons.com/ServerPacks/GT_New_Horizons_2.8.4_Server_Java_17-25.zip - -# Ensure target state directory exists. -sudo mkdir -p /srv/gtnh - -# Unpack into /srv/gtnh. Use bsdtar if present. -sudo bsdtar -xf GT_New_Horizons_2.8.4_Server_Java_17-25.zip -C /srv/gtnh - -# If bsdtar is unavailable, use comma to run unzip without permanently installing it: -# , unzip GT_New_Horizons_2.8.4_Server_Java_17-25.zip -d /tmp/gtnh-unpack -# sudo rsync -a /tmp/gtnh-unpack/ /srv/gtnh/ - -# Accept Mojang's EULA. Required by both GTNH/NixOS docs and Minecraft itself. -echo "eula=true" | sudo tee /srv/gtnh/eula.txt - -# Make all server files owned by the service account. -sudo chown -R gtnh:gtnh /srv/gtnh -sudo chmod -R u+rwX,g+rX,o-rwx /srv/gtnh - -# Sanity-check that Java 17+ GTNH startup files are present. -ls -la /srv/gtnh/lwjgl3ify-forgePatches.jar /srv/gtnh/java9args.txt -``` - -## 5. Configure `server.properties` - -Edit `/srv/gtnh/server.properties`: - -```sh -sudoedit /srv/gtnh/server.properties -``` - -Recommended baseline: - -```properties -# World folder name under /srv/gtnh. -# The migration below copies the Prism world into /srv/gtnh/world. -level-name=world - -# GTNH intended world generation. -# Sources: GTNH Server Setup FAQ and itzg GTNH defaults. -level-type=rwg - -# GTNH default/recommended difficulty. -difficulty=hard - -# Required/recommended for modded movement and GTNH server defaults. -allow-flight=true - -# GTNH/itzg default. Keep enabled unless there is a reason not to. -enable-command-block=true - -# Private local server safety. Add players with `whitelist add USERNAME`. -white-list=true - -# Keep small for a local/private server. -max-players=5 - -# Default Minecraft port; firewall module opens this. -server-port=25565 - -# Server list display text. -motd=GT New Horizons 2.8.4 on nemesis -``` - -Then fix ownership if needed: - -```sh -sudo chown gtnh:gtnh /srv/gtnh/server.properties -``` - -## 6. First boot test with a generated empty world - -Do this before copying the Prism world. It proves the server pack, Java, service, firewall, and console FIFO work. - -```sh -# Start GTNH. First launch can take several minutes. -sudo systemctl start gtnh-server.service - -# Follow logs. Wait until startup completes. -journalctl -fu gtnh-server.service - -# In another terminal, test console input through the FIFO. -echo "say GTNH server online" | sudo tee /run/gtnh-server.stdin - -# Stop cleanly before migration. -echo "stop" | sudo tee /run/gtnh-server.stdin - -# Confirm stopped or stopping cleanly. -systemctl status gtnh-server.service -``` - -## 7. Locate and back up the Prism singleplayer world - -NOTE: backups must include other folders than **/saves. check the gtnh wiki and local instance backup configuration. - -Prism instances are usually under `~/.local/share/PrismLauncher/instances` on Linux. - -```sh -# Find Prism saves directories. -find ~/.local/share/PrismLauncher/instances -maxdepth 3 -type d -name saves -``` - -Typical world path: - -```text -~/.local/share/PrismLauncher/instances//.minecraft/saves/ -``` - -Backup the world before touching it: - -```sh -# Replace and . -mkdir -p ~/gtnh-migration-backups - -tar -C ~/.local/share/PrismLauncher/instances//.minecraft/saves \ - -czf ~/gtnh-migration-backups/-before-server-migration.tar.gz \ - -``` - -## 8. Copy the Prism world to the server - -```sh -# Server must be stopped before replacing the world. -sudo systemctl stop gtnh-server.service - -# Move the generated test world aside instead of deleting it. -sudo mv /srv/gtnh/world /srv/gtnh/world.empty-test.$(date +%Y%m%d-%H%M%S) - -# Copy the Prism singleplayer save as the server's `world` directory. -# The trailing slashes are intentional: copy contents of into /srv/gtnh/world. -sudo rsync -a --info=progress2 \ - ~/.local/share/PrismLauncher/instances//.minecraft/saves// \ - /srv/gtnh/world/ - -# Give ownership to the service account. -sudo chown -R gtnh:gtnh /srv/gtnh/world - -# Start the migrated world. -sudo systemctl start gtnh-server.service -journalctl -fu gtnh-server.service -``` - -## 9. Whitelist and op yourself - -Run after the server is up: - -```sh -# Replace YOUR_USERNAME with the exact Minecraft username. -# Whitelist is recommended for private/self-hosted servers. -echo "whitelist add YOUR_USERNAME" | sudo tee /run/gtnh-server.stdin - -# Give yourself admin permissions for migration validation and future admin tasks. -echo "op YOUR_USERNAME" | sudo tee /run/gtnh-server.stdin - -# Force a save after changing admin files. -echo "save-all" | sudo tee /run/gtnh-server.stdin -``` - -Connect from Prism: - -```text -nemesis:25565 -``` - -or use the LAN IP of `nemesis`. - -## 10. Validate the migrated world - -Verify before deleting any backups: - -- Spawn location is expected. -- Inventory and armor are correct. -- Quest book progress is intact. -- Machines/multiblocks are present and valid. -- Dimensions are present. -- Claimed/chunk-loaded areas behave as expected. -- Server TPS is acceptable after initial chunk loading. - -Caveat: - -- Singleplayer-to-server migrations can sometimes expose Minecraft `level.dat` player data vs UUID-based `playerdata` differences. If inventory or position is wrong, stop the server and keep the original save backup. Fixing is possible with NBT tooling, but should be done carefully. - -## 11. Optional: add system-level GTNH backups - -GTNH's ServerUtilities includes backups, but an external host-level backup is still useful. - -Create `nix/modules/gtnh-backups.nix`: - -```nix -{ pkgs, ... }: - -let - # Backup destination outside the live server directory. - backupDir = "/srv/gtnh-backups"; -in -{ - config.flake.modules.nixos.gtnh-backups = { - # Ensure backup directory exists. Owned by root because backups are written by systemd. - systemd.tmpfiles.rules = [ - "d ${backupDir} 0750 root root -" - ]; - - systemd.services.gtnh-backup = { - description = "Backup GTNH server"; - serviceConfig = { - Type = "oneshot"; - - ExecStart = pkgs.writeShellScript "gtnh-backup" '' - set -euo pipefail - - ts="$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S)" - dest="${backupDir}/gtnh-$ts.tar.zst" - - # Ask the server to flush world data before taking a filesystem backup. - # This is not as strong as a fully quiesced backup, but is simple and low disruption. - echo "say Starting server backup" > /run/gtnh-server.stdin || true - echo "save-all" > /run/gtnh-server.stdin || true - sleep 10 - - # Archive /srv/gtnh as mutable state. - # Exclude GTNH's own backup folder to avoid recursive backup bloat. - ${pkgs.gnutar}/bin/tar \ - --exclude='/srv/gtnh/backups' \ - -C /srv \ - -I '${pkgs.zstd}/bin/zstd -T0 -10' \ - -cf "$dest" \ - gtnh - - # Keep 14 days of host-level backups. - ${pkgs.findutils}/bin/find ${backupDir} \ - -name 'gtnh-*.tar.zst' \ - -type f \ - -mtime +14 \ - -delete - - echo "say Server backup complete" > /run/gtnh-server.stdin || true - ''; - }; - }; - - systemd.timers.gtnh-backup = { - wantedBy = [ "timers.target" ]; - timerConfig = { - # Run daily at 03:30 local time. - OnCalendar = "03:30"; - - # If the machine was off at 03:30, run once after boot. - Persistent = true; - }; - }; - }; -} -``` - -Import it in `nemesis.modules`: - -```nix -modules = [ - # Existing nemesis modules... - cfg.modules.nixos.gtnh-server - - # Adds a daily compressed /srv/gtnh backup timer. - cfg.modules.nixos.gtnh-backups -]; -``` - -Apply: - -```sh -git add nix/modules/gtnh-backups.nix nix/hosts.nix -nix develop -c just format-nix -nix develop -c just check-nix -nix develop -c just rb -``` - -Manual backup test: - -```sh -sudo systemctl start gtnh-backup.service -ls -lh /srv/gtnh-backups -``` - -## 12. Admin command reference - -```sh -# Follow logs. -journalctl -fu gtnh-server.service - -# Service lifecycle. -sudo systemctl start gtnh-server.service -sudo systemctl stop gtnh-server.service -sudo systemctl restart gtnh-server.service -sudo systemctl status gtnh-server.service - -# Send Minecraft console commands through the FIFO. -echo "say hello" | sudo tee /run/gtnh-server.stdin -echo "list" | sudo tee /run/gtnh-server.stdin -echo "save-all" | sudo tee /run/gtnh-server.stdin -echo "stop" | sudo tee /run/gtnh-server.stdin - -# Check listener. -ss -ltnup | grep 25565 - -# Check backup timer. -systemctl list-timers | grep gtnh -``` - -## 13. Commit - -```sh -git status -git add nix/modules/gtnh-server.nix nix/modules/gtnh-backups.nix nix/hosts.nix docs/gtnh-server-migration.md -git commit -m "feat(gtnh-server): document nemesis GTNH migration" -```