diff --git a/maintainers/maintainer-list.nix b/maintainers/maintainer-list.nix index bcb3219ebeca8..e7abd7a5736e0 100644 --- a/maintainers/maintainer-list.nix +++ b/maintainers/maintainer-list.nix @@ -21287,6 +21287,12 @@ githubId = 16345849; name = "Parthiv Seetharaman"; }; + padowyt2 = { + email = "me@padow.ru"; + github = "PadowYT2"; + githubId = 71085027; + name = "PadowYT2"; + }; paepcke = { email = "git@paepcke.de"; github = "paepckehh"; diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index fa6f1989ba71e..83647d869acd6 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -104,6 +104,15 @@ "module-services-onedrive": [ "index.html#module-services-onedrive" ], + "module-services-pterodactyl": [ + "index.html#module-services-pterodactyl" + ], + "module-services-pterodactyl-basic-usage": [ + "index.html#module-services-pterodactyl-basic-usage" + ], + "module-services-pterodactyl-cli": [ + "index.html#module-services-pterodactyl-cli" + ], "module-services-tandoor-recipes-migrating-media-option-move": [ "index.html#module-services-tandoor-recipes-migrating-media-option-move", "index.html#module-services-tandoor-recipes-migrating-media-option-1" diff --git a/nixos/doc/manual/release-notes/rl-2611.section.md b/nixos/doc/manual/release-notes/rl-2611.section.md index 3fbd21ee4f1d9..fa2dbef294cde 100644 --- a/nixos/doc/manual/release-notes/rl-2611.section.md +++ b/nixos/doc/manual/release-notes/rl-2611.section.md @@ -34,6 +34,8 @@ - [Matrix Authentication Service](https://github.com/element-hq/matrix-authentication-service) is an OAuth2.0 and OpenID Connect provider for Matrix homeservers (such as Synapse). It replaces standard password authentication with modern OpenID Connect flows, and can delegate authentication to upstream OIDC providers. Available as [services.matrix-authentication-service](#opt-services.matrix-authentication-service.enable). +- [Pterodactyl](https://pterodactyl.io), a free, open-source game server management panel. Available as [services.pterodactyl.panel](#opt-services.pterodactyl.panel.enable) and [services.pterodactyl.wings](#opt-services.pterodactyl.wings.enable), a server control plane for Pterodactyl Panel. + ## Backward Incompatibilities {#sec-release-26.11-incompatibilities} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 692b4462c9288..a1737aaa452a7 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -945,6 +945,7 @@ ./services/misc/podgrab.nix ./services/misc/polaris.nix ./services/misc/portunus.nix + ./services/misc/pterodactyl-wings.nix ./services/misc/pufferpanel.nix ./services/misc/pykms.nix ./services/misc/radicle.nix @@ -1779,6 +1780,7 @@ ./services/web-apps/pretix.nix ./services/web-apps/privatebin.nix ./services/web-apps/prosody-filer.nix + ./services/web-apps/pterodactyl-panel.nix ./services/web-apps/readeck.nix ./services/web-apps/remark42.nix ./services/web-apps/reposilite.nix diff --git a/nixos/modules/services/misc/pterodactyl-wings.nix b/nixos/modules/services/misc/pterodactyl-wings.nix new file mode 100644 index 0000000000000..918cc98509eb9 --- /dev/null +++ b/nixos/modules/services/misc/pterodactyl-wings.nix @@ -0,0 +1,427 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.pterodactyl.wings; + + mainConfig = { + debug = cfg.debug; + app_name = cfg.appName; + uuid = cfg.uuid; + token_id = if cfg.tokenIdFile != null then "@TOKEN_ID@" else cfg.tokenId; + token = if cfg.tokenFile != null then "@TOKEN@" else cfg.token; + api = { + host = cfg.api.host; + port = cfg.api.port; + ssl = { + enabled = cfg.api.ssl.enable; + cert = cfg.api.ssl.certFile; + key = cfg.api.ssl.keyFile; + }; + upload_limit = cfg.api.uploadLimit; + trusted_proxies = cfg.api.trustedProxies; + }; + system = { + root_directory = cfg.rootDir; + log_directory = cfg.logDir; + data = "${cfg.rootDir}/volumes"; + archive_directory = "${cfg.rootDir}/archives"; + backup_directory = "${cfg.rootDir}/backups"; + tmp_directory = cfg.tmpDir; + username = cfg.user; + user = { + uid = config.users.users.${cfg.user}.uid; + gid = config.users.groups.${cfg.group}.gid; + }; + sftp = { + bind_address = cfg.system.sftp.host; + bind_port = cfg.system.sftp.port; + }; + docker = { + tmpfs_size = cfg.docker.tmpfsSize; + container_pid_limit = cfg.docker.containerPidLimit; + installer_limits = { + memory = cfg.docker.installerLimits.memory; + cpu = cfg.docker.installerLimits.cpu; + }; + }; + passwd.directory = "${cfg.runDir}/etc"; + machine_id.directory = "${cfg.runDir}/machine-id"; + use_openat2 = false; + }; + remote = cfg.remote; + ignore_panel_config_updates = true; + }; + + setupScript = pkgs.writeShellApplication { + name = "pterodactyl-wings-setup"; + runtimeInputs = with pkgs; [ + coreutils + replace-secret + ]; + text = '' + install -Dm640 -o ${cfg.user} -g ${cfg.group} ${ + (pkgs.formats.yaml { }).generate "config.yml" (lib.recursiveUpdate mainConfig cfg.extraConfig) + } ${cfg.rootDir}/config.yml + + ${lib.optionalString (cfg.tokenIdFile != null) '' + replace-secret '@TOKEN_ID@' ${lib.escapeShellArg cfg.tokenIdFile} ${cfg.rootDir}/config.yml + ''} + + ${lib.optionalString (cfg.tokenFile != null) '' + replace-secret '@TOKEN@' ${lib.escapeShellArg cfg.tokenFile} ${cfg.rootDir}/config.yml + ''} + ''; + }; + + cfgService = { + User = cfg.user; + Group = cfg.group; + StateDirectory = lib.removePrefix "/var/lib/" cfg.rootDir; + LogsDirectory = lib.removePrefix "/var/log/" cfg.logDir; + CacheDirectory = lib.removePrefix "/var/cache/" cfg.tmpDir; + RuntimeDirectory = lib.removePrefix "/run/" cfg.runDir; + ReadWritePaths = [ + cfg.rootDir + cfg.logDir + cfg.tmpDir + cfg.runDir + ]; + }; +in +{ + options.services.pterodactyl.wings = { + enable = lib.mkEnableOption "Pterodactyl Wings service"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.pterodactyl-wings; + defaultText = "pkgs.pterodactyl-wings"; + description = "Pterodactyl Wings package to use"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "pterodactyl-wings"; + description = "User to run Wings as"; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "pterodactyl-wings"; + description = "Group to run Wings as"; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to open the Wings API and SFTP ports in the firewall"; + }; + + rootDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/pterodactyl-wings"; + description = "The root directory where all of Wings's data is stored"; + }; + + logDir = lib.mkOption { + type = lib.types.path; + default = "/var/log/pterodactyl-wings"; + description = "Directory where logs for Wings and server installations are stored"; + }; + + tmpDir = lib.mkOption { + type = lib.types.path; + default = "/var/cache/pterodactyl-wings"; + description = "Directory where temporary files for server installations are stored"; + }; + + runDir = lib.mkOption { + type = lib.types.path; + default = "/run/pterodactyl-wings"; + description = "Directory where runtime files are stored"; + }; + + debug = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to run Wings in debug mode"; + }; + + appName = lib.mkOption { + type = lib.types.str; + default = "Pterodactyl"; + description = "The name of the daemon"; + }; + + uuid = lib.mkOption { + type = lib.types.str; + description = "A unique identifier for this node in the panel"; + }; + + tokenId = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "An identifier for the token"; + }; + + tokenIdFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the token ID"; + }; + + token = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The token for communicating with the panel"; + }; + + tokenFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the token"; + }; + + remote = lib.mkOption { + type = lib.types.str; + description = "The URL of the panel to connect to"; + }; + + api = { + host = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "The interface that Wings should bind to"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "The port that Wings should bind to"; + }; + + ssl = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to enable SSL for the API"; + }; + + certFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to the SSL certificate file"; + }; + + keyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to the SSL key file"; + }; + }; + + uploadLimit = lib.mkOption { + type = lib.types.int; + default = 100; + description = "The maximum size for files uploaded through the panel in MB"; + }; + + trustedProxies = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "A list of trusted proxy IP addresses"; + }; + }; + + system.sftp = { + host = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "The interface that Wings's SFTP should bind to"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 2022; + description = "The port that Wings's SFTP should bind to"; + }; + }; + + docker = { + tmpfsSize = lib.mkOption { + type = lib.types.int; + default = 100; + description = "The size of the temporary directory in MB for the container"; + }; + + containerPidLimit = lib.mkOption { + type = lib.types.int; + default = 512; + description = "Total number of processes that can be active in a container"; + }; + + installerLimits = { + memory = lib.mkOption { + type = lib.types.int; + default = 1024; + description = "The maximum amount of RAM the installation process can use"; + }; + + cpu = lib.mkOption { + type = lib.types.int; + default = 100; + description = "The maximum amount of CPU the installation process can use"; + }; + }; + }; + + extraConfig = lib.mkOption { + type = lib.types.attrsOf lib.types.anything; + default = { }; + description = "Extra configuration to be merged with the main configuration"; + }; + + extraConfigFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Extra configuration file to be merged with the other configuration"; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.uuid != ""; + message = "services.pterodactyl.wings.uuid must be set"; + } + { + assertion = cfg.remote != ""; + message = "services.pterodactyl.wings.remote must be set"; + } + { + assertion = cfg.tokenId == null || cfg.tokenIdFile == null; + message = "cannot set both services.pterodactyl.wings.tokenId and services.pterodactyl.wings.tokenIdFile"; + } + { + assertion = cfg.tokenId != null || cfg.tokenIdFile != null; + message = "must set either services.pterodactyl.wings.tokenId or services.pterodactyl.wings.tokenIdFile"; + } + { + assertion = cfg.token == null || cfg.tokenFile == null; + message = "cannot set both services.pterodactyl.wings.token and services.pterodactyl.wings.tokenFile"; + } + { + assertion = cfg.token != null || cfg.tokenFile != null; + message = "must set either services.pterodactyl.wings.token or services.pterodactyl.wings.tokenFile"; + } + ]; + + virtualisation.docker.enable = true; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ + cfg.api.port + cfg.system.sftp.port + ]; + + systemd.tmpfiles.settings."10-pterodactyl-wings" = + lib.attrsets.genAttrs + [ + "${cfg.rootDir}/machine-id" + "${cfg.rootDir}/volumes" + "${cfg.rootDir}/volumes/.sftp" + "${cfg.rootDir}/archives" + "${cfg.rootDir}/backups" + ] + (n: { + d = { + user = cfg.user; + group = cfg.group; + mode = "0755"; + }; + }) + // { + "${cfg.rootDir}".d = { + user = cfg.user; + group = cfg.group; + mode = "0750"; + }; + "${cfg.rootDir}/wings.db".z = { + user = cfg.user; + group = cfg.group; + mode = "0644"; + }; + "${cfg.rootDir}/states.json".z = { + user = cfg.user; + group = cfg.group; + mode = "0644"; + }; + "${cfg.runDir}".d = { + user = cfg.user; + group = cfg.group; + mode = "0755"; + }; + "${cfg.logDir}".d = { + user = cfg.user; + group = cfg.group; + mode = "0755"; + }; + "${cfg.tmpDir}".d = { + user = cfg.user; + group = cfg.group; + mode = "0755"; + }; + }; + + systemd.services.pterodactyl-wings-setup = { + description = "Pterodactyl Wings setup"; + before = [ "pterodactyl-wings.service" ]; + requiredBy = [ "pterodactyl-wings.service" ]; + + serviceConfig = cfgService // { + Type = "oneshot"; + ExecStart = lib.getExe setupScript; + RemainAfterExit = true; + }; + }; + + systemd.services.pterodactyl-wings = { + description = "Pterodactyl Wings service"; + after = [ + "network-online.target" + "docker.service" + "pterodactyl-wings-setup.service" + ]; + wants = [ "network-online.target" ]; + requires = [ + "docker.service" + "pterodactyl-wings-setup.service" + ]; + partOf = [ "docker.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = cfgService // { + ExecStart = "${cfg.package}/bin/wings --config ${cfg.rootDir}/config.yml"; + Restart = "on-failure"; + AmbientCapabilities = "CAP_CHOWN"; + EnvironmentFile = lib.optional (cfg.extraConfigFile != null) cfg.extraConfigFile; + }; + }; + + users.users = lib.mkIf (cfg.user == "pterodactyl-wings") { + ${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.rootDir; + extraGroups = [ "docker" ]; + }; + }; + + users.groups = lib.mkIf (cfg.group == "pterodactyl-wings") { + ${cfg.group} = { }; + }; + }; +} diff --git a/nixos/modules/services/web-apps/pterodactyl-panel.md b/nixos/modules/services/web-apps/pterodactyl-panel.md new file mode 100644 index 0000000000000..7e21d433a91ba --- /dev/null +++ b/nixos/modules/services/web-apps/pterodactyl-panel.md @@ -0,0 +1,44 @@ +# Pterodactyl Panel {#module-services-pterodactyl-panel} + +[Pterodactyl](https://pterodactyl.io/panel/1.0/getting_started.html) is a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users. + +## Basic usage {#module-services-pterodactyl-panel-basic-usage} + +The module defaults to Nginx, PHP-FPM, MariaDB, and Redis. +Only the application URL and some secrets must be provided. + +```nix +{ + services.pterodactyl.panel = { + enable = true; + app = { + url = "https://panel.example.com"; + keyFile = "/run/secrets/pterodactyl/app-key"; + }; + + hashids.saltFile = "/run/secrets/pterodactyl/hashids-salt"; + }; + + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; +} +``` + +`services.pterodactyl.panel.app.key` and `services.pterodactyl.panel.hashids.salt` are required. +Prefer the `*File` options so secrets do not end up in the Nix store. + +By default `services.pterodactyl.panel.app.environmentOnly` is set to `true`, which keeps the application configuration declarative and prevents the Panel from using the settings from the admin UI. + +## CLI {#module-services-pterodactyl-panel-cli} + +The module adds `pterodactyl-cli` to `environment.systemPackages`. +It runs `php artisan` from `services.pterodactyl.panel.dataDir` with all environment variables loaded. + +For example, to create a user: + +```sh +sudo pterodactyl-cli p:user:make +# > ... +``` diff --git a/nixos/modules/services/web-apps/pterodactyl-panel.nix b/nixos/modules/services/web-apps/pterodactyl-panel.nix new file mode 100644 index 0000000000000..4034414efc0be --- /dev/null +++ b/nixos/modules/services/web-apps/pterodactyl-panel.nix @@ -0,0 +1,748 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.pterodactyl.panel; + + env = + (lib.filterAttrs (n: v: v != null) { + APP_NAME = cfg.app.name; + APP_ENV = cfg.app.env; + APP_DEBUG = cfg.app.debug; + APP_KEY = if cfg.app.keyFile != null then "@APP_KEY@" else cfg.app.key; + APP_TIMEZONE = cfg.app.timezone; + APP_URL = cfg.app.url; + APP_ENVIRONMENT_ONLY = cfg.app.environmentOnly; + + DB_CONNECTION = "mysql"; + DB_HOST = if cfg.database.createLocally then "localhost" else cfg.database.host; + DB_PORT = cfg.database.port; + DB_DATABASE = cfg.database.name; + DB_USERNAME = cfg.database.user; + DB_PASSWORD = if cfg.database.passwordFile != null then "@DB_PASSWORD@" else cfg.database.password; + DB_SOCKET = if cfg.database.createLocally then "/run/mysqld/mysqld.sock" else null; + + REDIS_SCHEME = if cfg.redis.createLocally then "unix" else "tcp"; + REDIS_PATH = + if cfg.redis.createLocally then + config.services.redis.servers.${cfg.redis.name}.unixSocket + else + null; + REDIS_HOST = if cfg.redis.createLocally then null else cfg.redis.host; + REDIS_PORT = if cfg.redis.createLocally then null else cfg.redis.port; + REDIS_PASSWORD = if cfg.redis.passwordFile != null then "@REDIS_PASSWORD@" else cfg.redis.password; + + CACHE_DRIVER = cfg.cacheDriver; + QUEUE_CONNECTION = cfg.queueConnection; + SESSION_DRIVER = cfg.sessionDriver; + + HASHIDS_SALT = if cfg.hashids.saltFile != null then "@HASHIDS_SALT@" else cfg.hashids.salt; + HASHIDS_LENGTH = cfg.hashids.length; + + MAIL_MAILER = cfg.mail.mailer; + MAIL_HOST = cfg.mail.host; + MAIL_PORT = cfg.mail.port; + MAIL_USERNAME = cfg.mail.username; + MAIL_PASSWORD = if cfg.mail.passwordFile != null then "@MAIL_PASSWORD@" else cfg.mail.password; + MAIL_ENCRYPTION = cfg.mail.encryption; + MAIL_FROM_ADDRESS = cfg.mail.fromAddress; + MAIL_FROM_NAME = cfg.mail.fromName; + + RECAPTCHA_ENABLED = cfg.recaptcha.enabled; + RECAPTCHA_SECRET_KEY = + if cfg.recaptcha.secretKeyFile != null then "@RECAPTCHA_SECRET_KEY@" else cfg.recaptcha.secretKey; + RECAPTCHA_WEBSITE_KEY = + if cfg.recaptcha.websiteKeyFile != null then + "@RECAPTCHA_WEBSITE_KEY@" + else + cfg.recaptcha.websiteKey; + + TRUSTED_PROXIES = builtins.concatStringsSep "," cfg.trustedProxies; + PTERODACTYL_TELEMETRY_ENABLED = cfg.telemetry.enable; + }) + // cfg.extraEnvironment; + + setupScript = pkgs.writeShellApplication { + name = "pterodactyl-panel-setup"; + runtimeInputs = with pkgs; [ + coreutils + replace-secret + cfg.phpPackage + mariadb + ]; + text = '' + install -Dm640 -o ${cfg.user} -g ${cfg.group} ${ + pkgs.writeText "pterodactyl.env" ( + lib.generators.toKeyValue { + mkKeyValue = lib.generators.mkKeyValueDefault { + mkValueString = + v: + if builtins.isString v && lib.strings.hasInfix " " v then + ''"${v}"'' + else + lib.generators.mkValueStringDefault { } v; + } "="; + } env + ) + } ${cfg.dataDir}/.env + + ${lib.optionalString (cfg.app.keyFile != null) '' + replace-secret '@APP_KEY@' ${lib.escapeShellArg cfg.app.keyFile} ${cfg.dataDir}/.env + ''} + + ${lib.optionalString (cfg.database.passwordFile != null) '' + replace-secret '@DB_PASSWORD@' ${lib.escapeShellArg cfg.database.passwordFile} ${cfg.dataDir}/.env + ''} + + ${lib.optionalString (cfg.redis.passwordFile != null) '' + replace-secret '@REDIS_PASSWORD@' ${lib.escapeShellArg cfg.redis.passwordFile} ${cfg.dataDir}/.env + ''} + + ${lib.optionalString (cfg.hashids.saltFile != null) '' + replace-secret '@HASHIDS_SALT@' ${lib.escapeShellArg cfg.hashids.saltFile} ${cfg.dataDir}/.env + ''} + + ${lib.optionalString (cfg.mail.passwordFile != null) '' + replace-secret '@MAIL_PASSWORD@' ${lib.escapeShellArg cfg.mail.passwordFile} ${cfg.dataDir}/.env + ''} + + ${lib.optionalString (cfg.recaptcha.secretKeyFile != null) '' + replace-secret '@RECAPTCHA_SECRET_KEY@' ${lib.escapeShellArg cfg.recaptcha.secretKeyFile} ${cfg.dataDir}/.env + ''} + + ${lib.optionalString (cfg.recaptcha.websiteKeyFile != null) '' + replace-secret '@RECAPTCHA_WEBSITE_KEY@' ${lib.escapeShellArg cfg.recaptcha.websiteKeyFile} ${cfg.dataDir}/.env + ''} + + ${lib.optionalString (cfg.extraEnvironmentFile != null) '' + cat ${lib.escapeShellArg cfg.extraEnvironmentFile} >> ${cfg.dataDir}/.env + ''} + + php ${cfg.package}/artisan migrate --seed --force + php ${cfg.package}/artisan optimize:clear + ''; + }; + + pterodactylCli = pkgs.writeShellApplication { + name = "pterodactyl-cli"; + runtimeInputs = [ cfg.phpPackage ]; + text = '' + cd ${cfg.dataDir} + php ${cfg.package}/artisan "$@" + ''; + }; + + cfgService = { + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.package; + StateDirectory = lib.removePrefix "/var/lib/" cfg.dataDir; + ReadWritePaths = [ cfg.dataDir ]; + }; +in +{ + options.services.pterodactyl.panel = { + enable = lib.mkEnableOption "Pterodactyl Panel"; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.pterodactyl-panel; + defaultText = lib.literalExpression '' + pkgs.pterodactyl-panel.override { + dataDir = config.services.pterodactyl.panel.dataDir; + }; + ''; + description = "Pterodactyl Panel package to use"; + }; + + phpPackage = lib.mkOption { + type = lib.types.package; + readOnly = true; + default = pkgs.php83.buildEnv { + extensions = + { + enabled, + all, + }: + enabled + ++ (with all; [ + bcmath + curl + dom + gd + mbstring + mysqli + opcache + pdo + pdo_mysql + xml + tokenizer + openssl + zip + ]); + }; + defaultText = lib.literalExpression '' + pkgs.php83.buildEnv { + extensions = { + enabled, + all, + }: + enabled + ++ (with all; [ + bcmath + curl + dom + gd + mbstring + mysqli + opcache + pdo + pdo_mysql + xml + tokenizer + openssl + zip + ]); + }; + ''; + description = "The PHP package to use"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "pterodactyl-panel"; + description = "User to run the panel as"; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "pterodactyl-panel"; + description = "Group to run the panel as"; + }; + + enableNginx = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable Nginx and PHP-FPM"; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/pterodactyl-panel"; + description = "The root directory where all of the panel's data is stored"; + }; + + app = { + name = lib.mkOption { + type = lib.types.str; + default = "Pterodactyl"; + description = "The name of the panel"; + }; + + env = lib.mkOption { + type = lib.types.str; + default = "production"; + description = "The panel environment"; + }; + + debug = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to run the panel in debug mode"; + }; + + key = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The panel encryption key"; + }; + + keyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the panel encryption key"; + }; + + timezone = lib.mkOption { + type = lib.types.str; + default = "UTC"; + description = "The timezone for the panel"; + }; + + url = lib.mkOption { + type = lib.types.str; + description = "The URL of the panel"; + }; + + environmentOnly = lib.mkOption { + type = lib.types.bool; + default = true; + description = "The ability to manage settings from the panel"; + }; + }; + + database = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to create the database locally"; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "The host of the database"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 3306; + description = "The port of the database"; + }; + + name = lib.mkOption { + type = lib.types.str; + default = "panel"; + description = "The name of the database"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "pterodactyl-panel"; + description = "The user for the database"; + }; + + password = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The password for the database"; + }; + + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the database password"; + }; + }; + + redis = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to create the Redis instance locally"; + }; + + name = lib.mkOption { + type = lib.types.str; + default = "pterodactyl-panel"; + description = "The name of the Redis server to create"; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "The host of the Redis server"; + }; + + password = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The password for the Redis server"; + }; + + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the Redis password"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 6379; + description = "The port of the Redis server"; + }; + }; + + cacheDriver = lib.mkOption { + type = lib.types.str; + default = "redis"; + description = "The driver for the cache"; + }; + + queueConnection = lib.mkOption { + type = lib.types.str; + default = "redis"; + description = "The driver for the queue"; + }; + + sessionDriver = lib.mkOption { + type = lib.types.str; + default = "redis"; + description = "The driver for the session"; + }; + + hashids = { + salt = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The salt for the hash"; + }; + + saltFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the hash salt"; + }; + + length = lib.mkOption { + type = lib.types.int; + default = 8; + description = "The length of the generated hash"; + }; + }; + + mail = { + mailer = lib.mkOption { + type = lib.types.str; + default = "smtp"; + description = "The mailer to use"; + }; + + host = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The host of the mail server"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 25; + description = "The port of the mail server"; + }; + + username = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The username for the mail server"; + }; + + password = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The password for the mail server"; + }; + + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the mail password"; + }; + + encryption = lib.mkOption { + type = lib.types.str; + default = "tls"; + description = "The encryption for the mail server"; + }; + + fromAddress = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "The from address for the mail server"; + }; + + fromName = lib.mkOption { + type = lib.types.str; + default = "Pterodactyl Panel"; + description = "The from name for the mail server"; + }; + }; + + recaptcha = { + enabled = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable reCAPTCHA"; + }; + + secretKey = lib.mkOption { + type = lib.types.str; + default = "6LcJcjwUAAAAALOcDJqAEYKTDhwELCkzUkNDQ0J5"; + description = "The reCAPTCHA secret key"; + }; + + secretKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the reCAPTCHA secret key"; + }; + + websiteKey = lib.mkOption { + type = lib.types.str; + default = "6LcJcjwUAAAAAO_Xqjrtj9wWufUpYRnK6BW8lnfn"; + description = "The reCAPTCHA website key"; + }; + + websiteKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to a file containing the reCAPTCHA website key"; + }; + }; + + trustedProxies = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "A list of trusted proxy IP addresses"; + }; + + telemetry.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Whether to enable telemetry"; + }; + + extraEnvironment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = "Extra environment variables to be merged with the main environment variables"; + }; + + extraEnvironmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Additional environment file to be merged with other environment variables"; + }; + }; + + config = lib.mkIf cfg.enable { + services.pterodactyl.panel.package = lib.mkDefault ( + pkgs.pterodactyl-panel.override { + dataDir = cfg.dataDir; + } + ); + + assertions = [ + { + assertion = cfg.app.key == null || cfg.app.keyFile == null; + message = "cannot set both services.pterodactyl.panel.app.key and services.pterodactyl.panel.app.keyFile"; + } + { + assertion = cfg.app.key != null || cfg.app.keyFile != null; + message = "must set either services.pterodactyl.panel.app.key or services.pterodactyl.panel.app.keyFile"; + } + { + assertion = cfg.database.password == null || cfg.database.passwordFile == null; + message = "cannot set both services.pterodactyl.panel.database.password and services.pterodactyl.panel.database.passwordFile"; + } + { + assertion = cfg.redis.password == null || cfg.redis.passwordFile == null; + message = "cannot set both services.pterodactyl.panel.redis.password and services.pterodactyl.panel.redis.passwordFile"; + } + { + assertion = cfg.hashids.salt == null || cfg.hashids.saltFile == null; + message = "cannot set both services.pterodactyl.panel.hashids.salt and services.pterodactyl.panel.hashids.saltFile"; + } + { + assertion = cfg.hashids.salt != null || cfg.hashids.saltFile != null; + message = "must set either services.pterodactyl.panel.hashids.salt or services.pterodactyl.panel.hashids.saltFile"; + } + { + assertion = cfg.mail.password == null || cfg.mail.passwordFile == null; + message = "cannot set both services.pterodactyl.panel.mail.password and services.pterodactyl.panel.mail.passwordFile"; + } + ]; + + services.mysql = lib.optionalAttrs cfg.database.createLocally { + enable = true; + package = pkgs.mariadb; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { + name = cfg.database.user; + ensurePermissions."${cfg.database.name}.*" = "ALL PRIVILEGES"; + } + ]; + }; + + services.redis.servers."${cfg.redis.name}" = lib.mkIf cfg.redis.createLocally ( + { + enable = true; + user = cfg.user; + group = cfg.group; + } + // lib.optionalAttrs (cfg.redis.password != null) { requirePass = cfg.redis.password; } + // lib.optionalAttrs (cfg.redis.passwordFile != null) { requirePassFile = cfg.redis.passwordFile; } + ); + + systemd.tmpfiles.settings."10-pterodactyl-panel" = + lib.attrsets.genAttrs + [ + "${cfg.dataDir}/storage" + "${cfg.dataDir}/storage/app" + "${cfg.dataDir}/storage/app/public" + "${cfg.dataDir}/storage/app/private" + "${cfg.dataDir}/storage/clockwork" + "${cfg.dataDir}/storage/framework" + "${cfg.dataDir}/storage/framework/cache" + "${cfg.dataDir}/storage/framework/sessions" + "${cfg.dataDir}/storage/framework/views" + "${cfg.dataDir}/storage/logs" + "${cfg.dataDir}/bootstrap" + "${cfg.dataDir}/bootstrap/cache" + ] + (n: { + d = { + user = cfg.user; + group = cfg.group; + mode = "0770"; + }; + }) + // { + "${cfg.dataDir}".d = { + user = cfg.user; + group = cfg.group; + mode = "0750"; + }; + }; + + systemd.services.pterodactyl-panel-setup = { + description = "Pterodactyl Panel setup"; + requiredBy = lib.optional cfg.enableNginx "phpfpm-pterodactyl-panel.service"; + before = lib.optional cfg.enableNginx "phpfpm-pterodactyl-panel.service"; + after = [ "mysql.service" ]; + restartTriggers = [ cfg.package ]; + + serviceConfig = cfgService // { + Type = "oneshot"; + ExecStart = lib.getExe setupScript; + RemainAfterExit = true; + }; + }; + + systemd.services.pteroq = { + description = "Pterodactyl Queue Worker"; + after = [ + "pterodactyl-panel-setup.service" + "mysql.service" + "redis-pterodactyl-panel.service" + ]; + wants = [ "pterodactyl-panel-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = cfgService // { + ExecStart = "${cfg.phpPackage}/bin/php ${cfg.package}/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3"; + Restart = "always"; + }; + }; + + systemd.services.pterodactyl-panel-cron = { + description = "Pterodactyl Panel cron job"; + after = [ + "pterodactyl-panel-setup.service" + "mysql.service" + "redis-pterodactyl-panel.service" + ]; + wants = [ "pterodactyl-panel-setup.service" ]; + + serviceConfig = cfgService // { + Type = "oneshot"; + ExecStart = "${cfg.phpPackage}/bin/php ${cfg.package}/artisan schedule:run"; + }; + }; + + systemd.timers.pterodactyl-panel-cron = { + description = "Pterodactyl Panel cron timer"; + wantedBy = [ "timers.target" ]; + restartTriggers = [ cfg.package ]; + + timerConfig = { + OnCalendar = "minutely"; + Persistent = true; + }; + }; + + services.phpfpm.pools.pterodactyl-panel = lib.mkIf cfg.enableNginx { + user = cfg.user; + group = cfg.group; + phpPackage = cfg.phpPackage; + settings = { + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + "pm" = "dynamic"; + "pm.max_children" = 5; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 1; + "pm.max_spare_servers" = 3; + }; + }; + + systemd.services."phpfpm-pterodactyl-panel" = lib.mkIf cfg.enableNginx { + requires = [ "pterodactyl-panel-setup.service" ]; + }; + + services.nginx = lib.mkIf cfg.enableNginx { + enable = true; + recommendedTlsSettings = lib.mkDefault true; + recommendedOptimisation = lib.mkDefault true; + recommendedGzipSettings = lib.mkDefault true; + virtualHosts."${builtins.replaceStrings [ "https://" "http://" ] [ "" "" ] cfg.app.url}" = { + root = "${cfg.package}/public"; + extraConfig = '' + index index.php; + client_max_body_size 100m; + client_body_timeout 120s; + sendfile off; + ''; + locations = { + "/" = { + tryFiles = "$uri $uri/ /index.php?$query_string"; + index = "index.php"; + extraConfig = '' + sendfile off; + ''; + }; + "~ \\.php$" = { + extraConfig = '' + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass unix:${config.services.phpfpm.pools.pterodactyl-panel.socket}; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param HTTP_PROXY ""; + fastcgi_intercept_errors off; + fastcgi_buffer_size 16k; + fastcgi_buffers 4 16k; + fastcgi_connect_timeout 300; + fastcgi_send_timeout 300; + fastcgi_read_timeout 300; + include ${pkgs.nginx}/conf/fastcgi_params; + ''; + }; + }; + }; + }; + + environment.systemPackages = [ pterodactylCli ]; + + services.pterodactyl.panel.group = lib.mkIf cfg.enableNginx ( + lib.mkDefault config.services.nginx.group + ); + + users.users = lib.mkIf (cfg.user == "pterodactyl-panel") { + ${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + extraGroups = lib.optionals cfg.redis.createLocally [ "redis" ]; + }; + }; + + users.groups = lib.mkIf (cfg.group == "pterodactyl-panel") { + ${cfg.group} = { }; + }; + }; + + meta.doc = ./pterodactyl-panel.md; +} diff --git a/pkgs/by-name/pt/pterodactyl-panel/package.nix b/pkgs/by-name/pt/pterodactyl-panel/package.nix new file mode 100644 index 0000000000000..dc87a0613b27c --- /dev/null +++ b/pkgs/by-name/pt/pterodactyl-panel/package.nix @@ -0,0 +1,75 @@ +{ + lib, + stdenvNoCC, + fetchFromGitHub, + php83, + fetchYarnDeps, + yarnConfigHook, + yarnBuildHook, + nodejs_24, + dataDir ? "/var/lib/pterodactyl-panel", +}: +stdenvNoCC.mkDerivation (finalAttrs: { + __structuredAttrs = true; + strictDeps = true; + + pname = "pterodactyl-panel"; + version = "1.14.1"; + + src = fetchFromGitHub { + owner = "pterodactyl"; + repo = "panel"; + tag = "v${finalAttrs.version}"; + hash = "sha256-vAnkTkieyhvpx4QAaqRKj7qaZrMeN9S8kt46QLTdsyg="; + }; + + buildInputs = [ php83 ]; + nativeBuildInputs = [ + nodejs_24 + yarnConfigHook + yarnBuildHook + php83.packages.composer + php83.composerHooks2.composerInstallHook + ]; + + composerVendor = php83.mkComposerVendor { + inherit (finalAttrs) pname src version; + composerNoDev = true; + composerNoPlugins = true; + composerNoScripts = true; + composerStrictValidation = true; + strictDeps = true; + vendorHash = "sha256-65fDCa9Lp2UvCGLcRGuqq+E2S+A3NX5Bo7qBeazuZ4s="; + }; + + offlineCache = fetchYarnDeps { + yarnLock = "${finalAttrs.src}/yarn.lock"; + hash = "sha256-OvoTRw66wi3zLeWUpWGHIbYFYqSYrlEOEUYJ1Hg0ApM="; + }; + + env.NODE_OPTIONS = "--openssl-legacy-provider"; + yarnBuildScript = "build:production"; + + installPhase = '' + runHook preInstall + + chmod -R u+w $out/share + mv $out/share/php/pterodactyl-panel/* $out/ + + rm -rf $out/share $out/storage $out/bootstrap/cache $out/node_modules + ln -s ${dataDir}/storage $out/storage + ln -s ${dataDir}/bootstrap/cache $out/bootstrap/cache + ln -s ${dataDir}/.env $out/.env + + runHook postInstall + ''; + + meta = { + description = "Free, open-source game server management panel"; + homepage = "https://pterodactyl.io"; + changelog = "https://github.com/pterodactyl/panel/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ padowyt2 ]; + platforms = lib.platforms.all; + }; +}) diff --git a/pkgs/by-name/pt/pterodactyl-wings/package.nix b/pkgs/by-name/pt/pterodactyl-wings/package.nix new file mode 100644 index 0000000000000..030b566489938 --- /dev/null +++ b/pkgs/by-name/pt/pterodactyl-wings/package.nix @@ -0,0 +1,37 @@ +{ + lib, + buildGoModule, + fetchFromGitHub, +}: +buildGoModule (finalAttrs: { + __structuredAttrs = true; + strictDeps = true; + + pname = "pterodactyl-wings"; + version = "1.13.1"; + + src = fetchFromGitHub { + owner = "pterodactyl"; + repo = "wings"; + tag = "v${finalAttrs.version}"; + hash = "sha256-WADZUUaQZiJEcl3IeyH6L8SNJxYvWzsfGdWIYNrdRVs="; + }; + + vendorHash = "sha256-BtATik0egFk73SNhawbGnbuzjoZioGFWeA4gZOaofTI="; + + ldflags = [ + "-s" + "-w" + "-X github.com/pterodactyl/wings/system.Version=${finalAttrs.version}" + ]; + + meta = { + description = "Server control plane for Pterodactyl Panel"; + homepage = "https://pterodactyl.io"; + changelog = "https://github.com/pterodactyl/wings/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ padowyt2 ]; + mainProgram = "wings"; + platforms = lib.platforms.linux; + }; +})