diff --git a/.gitignore b/.gitignore
index 98a40ca6..35b772fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
/docs/.vitepress/cache
/docs/.vitepress/dist
/target
+/result
node_modules/
*.zst
.DS_Store
diff --git a/docs/about/installation.md b/docs/about/installation.md
index b198cdd7..b560f5e4 100644
--- a/docs/about/installation.md
+++ b/docs/about/installation.md
@@ -62,6 +62,70 @@ To use this configuration:
3. Create a `server.toml` file with your configuration
4. Run `docker compose up -d` to start the server
+## Using NixOS
+
+The flake provides a package, nixpkgs overlay, and NixOS module. The module runs PicoLimbo as a systemd service and exposes all server settings through `services.picolimbo.settings`, which is serialised to `/etc/picolimbo/server.toml` at activation time.
+
+To use it:
+
+1. Add PicoLimbo as a flake input
+
+```nix
+{
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ picolimbo.url = "github:Quozul/PicoLimbo";
+ };
+}
+```
+
+2. Import the NixOS module
+
+```nix
+imports = [
+ inputs.picolimbo.nixosModules.default
+];
+```
+
+3. Enable the service
+
+```nix
+{
+ services.picolimbo = {
+ enable = true;
+
+ settings = {
+ bind = "0.0.0.0:25565";
+
+ welcome_message = "You were spawned in Limbo.";
+ server_list = {
+ max_players = 50;
+ };
+ };
+ };
+}
+```
+
+It is recommended to store secrets (e.g. for forwarding) using `sops-nix` or with systemd credentials.
+
+```nix
+{
+ services.picolimbo.settings = {
+ bind = "0.0.0.0:25565";
+
+ forwarding = {
+ method = "MODERN";
+ secret = "\${VELOCITY_SECRET}";
+ };
+ };
+
+ systemd.services.picolimbo = {
+ serviceConfig.LoadCredential = "velocity-secret:/path/to/velocity-secret";
+ environment.VELOCITY_SECRET = "%d/velocity-secret";
+ };
+}
+```
+
## Binary / Standalone
### GitHub Releases
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 00000000..4fe7997b
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1777954456,
+ "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 00000000..a254376b
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,46 @@
+{
+ description = "Lightweight Minecraft limbo server";
+
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+
+ outputs =
+ { self, nixpkgs, ... }:
+ let
+ systems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ "aarch64-darwin"
+ ];
+ forAllSystems = nixpkgs.lib.genAttrs systems;
+ in
+ {
+ overlays.default = final: _prev: {
+ picolimbo = final.callPackage ./nix/package.nix { };
+ };
+
+ packages = forAllSystems (
+ system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default;
+ in
+ {
+ default = pkgs.picolimbo;
+ }
+ );
+
+ nixosModules.default =
+ {
+ pkgs,
+ config,
+ lib,
+ ...
+ }:
+ {
+ imports = [ ./nix/module.nix ];
+ config = lib.mkIf config.services.picolimbo.enable {
+ nixpkgs.overlays = [ self.overlays.default ];
+ services.picolimbo.package = lib.mkDefault pkgs.picolimbo;
+ };
+ };
+ };
+}
diff --git a/nix/module.nix b/nix/module.nix
new file mode 100644
index 00000000..2e091c1f
--- /dev/null
+++ b/nix/module.nix
@@ -0,0 +1,111 @@
+{
+ config,
+ pkgs,
+ lib,
+ ...
+}:
+
+let
+ inherit (lib)
+ literalExpression
+ mkDefault
+ mkEnableOption
+ mkIf
+ mkOption
+ mkPackageOption
+ types
+ ;
+ cfg = config.services.picolimbo;
+ settingsFormat = pkgs.formats.toml { };
+ configFile = settingsFormat.generate "server.toml" cfg.settings;
+ listenPort = lib.toInt (lib.last (lib.splitString ":" cfg.settings.bind));
+in
+{
+ options.services.picolimbo = {
+ enable = mkEnableOption "PicoLimbo, a lightweight Minecraft limbo server written in Rust";
+
+ package = mkPackageOption pkgs "picolimbo" { };
+
+ openFirewall = mkOption {
+ type = types.bool;
+ default = false;
+ example = true;
+ description = "Open the firewall port derived from {option}`services.picolimbo.settings.bind`.";
+ };
+
+ settings = mkOption {
+ type = settingsFormat.type;
+ default = { };
+ description = ''
+ PicoLimbo configuration as a Nix attribute set, serialised to
+ {file}`/etc/picolimbo/server.toml` at activation time.
+
+ Environment variable placeholders (`''${VAR}`) are expanded by the server at start-up.
+
+ Full reference: https://picolimbo.quozul.dev/config/introduction.html
+ '';
+ example = literalExpression ''
+ {
+ bind = "0.0.0.0:25565";
+ welcome_message = "You are in limbo!";
+ default_game_mode = "spectator";
+
+ forwarding = {
+ method = "MODERN";
+ secret = "''${VELOCITY_SECRET}";
+ };
+
+ server_list.message_of_the_day = "My Server";
+
+ tab_list = {
+ enabled = true;
+ header = "Limbo";
+ footer = "Reconnecting soon…";
+ };
+ }
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ services.picolimbo.settings = {
+ # Provide a default bind address so the generated TOML is never empty.
+ # An empty file causes the server to attempt a write back to the path,
+ # which fails against the read-only /etc location.
+ bind = mkDefault "0.0.0.0:25565";
+ };
+
+ assertions = [
+ {
+ assertion =
+ (cfg.settings.forwarding or { }).method or "NONE" != "MODERN"
+ || (cfg.settings.forwarding or { }) ? secret;
+ message = "services.picolimbo.settings.forwarding.secret must be set when method is \"MODERN\"";
+ }
+ {
+ assertion =
+ (cfg.settings.forwarding or { }).method or "NONE" != "BUNGEE_GUARD"
+ || (cfg.settings.forwarding or { }).tokens or [ ] != [ ];
+ message = "services.picolimbo.settings.forwarding.tokens must be non-empty when method is \"BUNGEE_GUARD\"";
+ }
+ ];
+
+ environment.etc."picolimbo/server.toml".source = configFile;
+
+ systemd.services.picolimbo = {
+ description = "PicoLimbo, a lightweight Minecraft limbo server written in Rust";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+
+ serviceConfig = {
+ ExecStart = "${lib.getExe cfg.package} --config /etc/picolimbo/server.toml";
+ StateDirectory = "picolimbo";
+ WorkingDirectory = "/var/lib/picolimbo";
+ DynamicUser = true;
+ Restart = "always";
+ };
+ };
+
+ networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ listenPort ];
+ };
+}
diff --git a/nix/package.nix b/nix/package.nix
new file mode 100644
index 00000000..d8fb3e06
--- /dev/null
+++ b/nix/package.nix
@@ -0,0 +1,30 @@
+{
+ lib,
+ rustPlatform,
+}:
+
+rustPlatform.buildRustPackage {
+ pname = "picolimbo";
+ version = "1.12.2+mc26.1.2";
+
+ src = lib.cleanSource ../.;
+
+ cargoLock.lockFile = ../Cargo.lock;
+
+ cargoBuildFlags = [
+ "--bin"
+ "pico_limbo"
+ ];
+
+ meta = with lib; {
+ description = "A lightweight Minecraft limbo server written in Rust";
+ homepage = "https://github.com/Quozul/PicoLimbo";
+ license = licenses.mit;
+ mainProgram = "pico_limbo";
+ platforms = [
+ "x86_64-linux"
+ "aarch64-linux"
+ "aarch64-darwin"
+ ];
+ };
+}