# Pterodactyl 1.x * [Install the panel](./panel/getting-started.mdx) * [Client API](./api/client-general/get-list-servers.mdx) Use the version selector to switch to the Pterodactyl 2.0 documentation. # Pterodactyl 2.0 Pterodactyl 2.0 is not released yet. These pages describe the development version and may change before release. This is the documentation for Pterodactyl Panel 2.0. If you run 1.x, switch to the 1.x documentation with the version selector. ## Project Information [#project-information] * [Introduction](./project/index.mdx) explains what Pterodactyl is and which games it supports. * [How Pterodactyl Works](./project/how-it-works.mdx) explains the parts of Pterodactyl and the terms these pages use. ## Panel [#panel] * [Getting Started](./panel/getting-started.mdx) installs the Panel on your own server. * [Docker Deployment](./panel/docker.mdx) runs the Panel from its Docker image instead. * [Webserver Configuration](./panel/webserver-configuration.mdx) sets up NGINX, Apache, or Caddy. * [Updating the Panel](./panel/updating.mdx) updates the Panel to a newer 2.x release. ## Wings [#wings] * [Installing Wings](./wings/installing.mdx) sets up a node to run your game servers. * [Upgrading Wings](./wings/upgrading.mdx) updates Wings to a new version. ## Extensions [#extensions] * [Extensions](./extensions/index.mdx) explains how to install and manage extensions. * [Building Extensions](./extensions/building.mdx) shows how to build your own. ## Upgrading From 1.x [#upgrading-from-1x] * [Changes From 1.x](./upgrading/changes-from-v1.mdx) lists what differs in 2.0 and who it affects. * [Upgrading From 1.x](./upgrading/upgrading-from-v1.mdx) walks through the upgrade step by step. Do not use the 1.x installation or update steps, or `php artisan p:upgrade`, to install 2.0. ## Community Guides [#community-guides] * [Creating SSL Certificates](./guides/tutorials/ssl-certificates.mdx) gets free certificates for the Panel and Wings. * [Setting Up MySQL](./guides/tutorials/mysql-setup.mdx) adds database hosts so servers can have their own databases. * [Creating a Custom Egg](./guides/egg-creation/creating-custom-egg.mdx) shows how to import and build eggs. * [Themes](./guides/customization/themes.mdx) changes the Panel's colors without rebuilding it. ## API Reference [#api-reference] Start with the [API Overview](./api/index.mdx). It explains the three APIs and how to authenticate. # API Overview ## Introduction [#introduction] The Panel has two APIs. Choose the one that matches what you want to do: | API | Path | Use it to | | ----------- | ------------------ | ------------------------------------------------------------------------------- | | Client | `/api/client` | Manage your account and the servers you have access to. | | Application | `/api/application` | Manage users, nodes, locations, and servers, for example from a billing system. | The `v1` in this documentation's address is not part of the API path. ## Authentication [#authentication] Send your API key in the `Authorization` header of every request: ```http Authorization: Bearer YOUR_API_KEY ``` Each API uses its own keys: | API | Key | Created in | | ----------- | ------------------------------------------------------ | ---------------------------------------------- | | Client | A Client API key. It can only do what its user can do. | **Account → API Credentials** (`/account/api`) | | Application | An Application API key. | **Admin → Application API** (`/admin/api`) | Responses are JSON. ## Where to Start [#where-to-start] * [List your servers](./client-general/get-list-servers.mdx) with the Client API. * [List users](./application-users/get-list-users.mdx) with the Application API. If you are upgrading to Pterodactyl 2.0, read [Changes From 1.x](../../v2/upgrading/changes-from-v1.mdx). The nest endpoints have been removed. # Additional Configuration ## Backups [#backups] Pterodactyl Panel allows users to create backups of their servers. In order to create backups, a backup storage method has to be configured. When changing Pterodactyl Panel's backup storage method, users may still download or delete existing backups from the prior storage driver. In the instance of migrating from S3 to local backups, S3 credentials must remain configured after switching to the local backup storage method. ### Using Local Backups [#using-local-backups] By default, Pterodactyl Panel uses local storage via Wings for backups. That said, this method of backup storage can be explicitly set with the following configuration in the `.env` file: ```bash # Sets your panel to use local storage via Wings for backups APP_BACKUP_DRIVER=wings ``` Do note that, when using local storage via Wings, the destination for backups is set in Wings' `config.yml` with the following setting key: ```yml system: backup_directory: /path/to/backup/storage ``` ### Using S3 Backups [#using-s3-backups] AWS S3 (or compatible storage) can be used to store remote or cloud-based backups. The following configuration options have to be set in the `.env` file or as environment variables in order to enable it: ```bash # Sets your panel to use s3 for backups APP_BACKUP_DRIVER=s3 # Info to actually use s3 AWS_DEFAULT_REGION= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_BACKUPS_BUCKET= AWS_ENDPOINT= ``` For some configurations, you might have to change your S3 URL from `bucket.domain.com` to `domain.com/bucket`. To accomplish this, add `AWS_USE_PATH_STYLE_ENDPOINT=true` to your `.env` file. #### Multipart Upload [#multipart-upload] The S3 backup is using the S3 multipart upload capabilities. In rare situations, you might want to adjust the size of a single part or the lifespan of the generated pre-signed URLs. The default part size is 5GB, and the default pre-signed URL lifespan is 60 minutes. You can configure the maximal part size using the `BACKUP_MAX_PART_SIZE` environment variable. You must specify the size in bytes. To define the pre-signed URL lifespan, use the `BACKUP_PRESIGNED_URL_LIFESPAN` variable. The expected unit is minutes. The following `.env` snippet configures 1GB parts and uses 120 minutes as the pre-signed URL lifespan: ```bash BACKUP_MAX_PART_SIZE=1073741824 BACKUP_PRESIGNED_URL_LIFESPAN=120 ``` #### Storage Class [#storage-class] Should you need to specify a storage class, use the `AWS_BACKUPS_STORAGE_CLASS` environment variable. Default option is `STANDARD` (S3 Standard). The following `.env` snippet sets the class to `STANDARD_IA` (this is an example). ```bash # STANDARD_IA is an example. AWS_BACKUPS_STORAGE_CLASS=STANDARD_IA ``` ## Reverse Proxy Setup [#reverse-proxy-setup] When running Pterodactyl behind a reverse proxy, such as [Cloudflare's Flexible SSL](https://support.cloudflare.com/hc/en-us/articles/200170416-What-do-the-SSL-options-mean-) or Nginx/Apache/Caddy, etc., you will need to make a quick modification to the Panel to ensure things continue to work as expected. By default, when using these reverse proxies, your Panel will not correctly handle requests. You'll most likely be unable to login or see security warnings in your browser console as it attempts to load insecure assets. This is because the internal logic the Panel uses to determine how links should be generated thinks it is running over HTTP and not over HTTPS. You will need to edit the `.env` file in the Panel's root directory to contain `TRUSTED_PROXIES=*` at minimum. We highly suggest providing a specific IP address (or comma-separated list of IPs) rather than allowing `*`. For example, if your proxy is running on the same machine as the server, the chances are that something like `TRUSTED_PROXIES=127.0.0.1` will work for you. ### NGINX Specific Configuration [#nginx-specific-configuration] For Pterodactyl to properly respond to an NGINX reverse proxy, the NGINX `location` config must contain the following lines: ```nginx proxy_set_header X-Real-IP $remote_addr; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect off; proxy_buffering off; proxy_request_buffering off; ``` ### Cloudflare Specific Configuration [#cloudflare-specific-configuration] If you're using Cloudflare's Flexible SSL you should set `TRUSTED_PROXIES` to contain [their IP addresses](https://www.cloudflare.com/ips/). Below is an example of how to set this. ```text TRUSTED_PROXIES=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22 ``` ## reCAPTCHA [#recaptcha] The Panel uses invisible reCAPTCHA to secure the login page from brute-force attacks. If the login attempt is considered suspicious, users may be required to perform a reCAPTCHA challenge. ### Configuring reCAPTCHA [#configuring-recaptcha] While we provide a global Site Key and Secret Key by default, we highly recommend changing it for your own setup. You can generate your own keys in the [reCAPTCHA Admin Console](https://www.google.com/recaptcha/admin). The keys can then be applied using the Settings in the admin panel. The reCAPTCHA settings can be found on the **Advanced** tab. ### Disabling reCAPTCHA [#disabling-recaptcha] We do not recommend disabling reCAPTCHA. It is a security mechanism that makes it harder to perform brute-force attacks on user accounts. If users have trouble logging in, or your Panel isn't exposed to the internet, it can make sense to disable reCAPTCHA. reCAPTCHA can easily be disabled using the admin panel. In the Settings, select the **Advanced** tab and set the **Status** of reCAPTCHA to **disabled**. #### Editing your database [#editing-your-database] If you cannot access your panel, you can modify the database directly using the following commands. ```sql # If using MariaDB (v11.0.0+) mariadb -u root -p # If using MySQL mysql -u root -p ``` ```sql INSERT INTO panel.settings (`key`, value) VALUES ('settings::recaptcha:enabled', 'false') ON DUPLICATE KEY UPDATE value = 'false'; ``` ## 2FA [#2fa] If possible you should use the panel to update your 2FA settings. If you can't access your panel for what ever reason you can use the following steps. ### Disable 2FA requirement [#disable-2fa-requirement] ```sql # If using MariaDB (v11.0.0+) mariadb -u root -p # If using MySQL mysql -u root -p ``` ```sql INSERT INTO panel.settings (`key`, value) VALUES ('settings::pterodactyl:auth:2fa_required', 0) ON DUPLICATE KEY UPDATE value = 0; ``` ### Disable 2FA for a specific user [#disable-2fa-for-a-specific-user] Run the following command in your `/var/www/pterodactyl` directory. ```bash php artisan p:user:disable2fa ``` ## Telemetry [#telemetry] Since 1.11, the Panel collects anonymous metrics about the Panel and all connected nodes. This feature is enabled by default, but can be disabled. The data collected by this feature is not sold or used for advertising purposes. Aggregate statistics may be made public or shared with third-parties for the purposes of improving the software. ### How does it work? [#how-does-it-work] The Telemetry system works by first generating a random UUIDv4 identifier for the Panel installation. This identifier is stored in the database so people load-balancing multiple Panel instances can still have a unique identifier. This identifier is then sent to a remote server, along the associated telemetry data. The telemetry data is collected every 24 hours, there is no ongoing collection or local storage of the telemetry data, we collect the data right before we send it to the remote server. Currently, all telemetry collection logic is handled by the [TelemetryCollectionService](https://github.com/pterodactyl/panel/blob/develop/app/Services/Telemetry/TelemetryCollectionService.php#L53) on the panel. This service is responsible for collecting all the data that is sent to the remote server. ### What data is collected? [#what-data-is-collected] If you wish to see the full data that is collected, please look at the TelemetryCollectionService (as linked above), or use the `php artisan p:telemetry` command to view the exact data that will be sent to the remote server. As of 2022-12-12, the data collected consists of: * Unique identifier for the Panel * Version of the Panel * PHP version * Backup storage driver (S3, Local, etc.) * Cache driver (Redis, Memcached, etc.) * Database driver and version (MySQL, MariaDB, PostgreSQL, etc.) * Resources * Allocations * Total number * Total number of used allocations (assigned to a server) * Backups * Total number * Sum of the total amount of bytes stored by backups * Eggs * Total number * ~~Map of egg UUIDs to the number of servers using that egg~~ (removed in 1.11.2) * Locations * Total number * Mounts * Total number * Nests * Total number * ~~Map of nest UUIDs to the number of servers using eggs in that nest~~ (removed in 1.11.2) * Nodes * Total number * Servers * Total number * Number of servers that are suspended * Users * Total number * Number of users that are admins * Nodes * Node UUID * Version of Wings on the node * Docker * Version * Cgroups * Driver * Version * Containers * Total * Running * Paused * Stopped * Storage * Driver * Filesystem * runc * Version * System * Architecture (`amd64`, `arm64`, etc.) * CPU Threads * Memory Bytes * Kernel Version * Operating System (Debian, Fedora, RHEL, Ubuntu, etc.) * Operating System Type (bsd, linux, windows, etc.) ### How is the data stored? [#how-is-the-data-stored] Currently, the data is stored with Cloudflare, we ingest all telemetry data with a Worker which does basic processing such as validation and then inserts it into Cloudflare D1. Right now, there is not an API or visualization for any of the data collected, and it can only be manually queried. Only Matthew is able to query the data at this time, but we are working on alternatives to make this data more accessible. ### Why? [#why] The primary reason for collecting this data is to help us make better decisions about the future of this software. With the release of 1.11, the minimum PHP version requirement jumped from 7.4 to 8.0, however, we wanted to add a feature that required PHP 8.1 which would've made the version requirement jump larger and potentially cause issues for some users. By collecting this data, we can hopefully have more insight to how changes like this will affect the community and make better decisions in the future. This is especially important for information like the architecture, kernel version, and operating system nodes are using. For example, we want to utilize a feature that is only present in some filesystems, but we have no idea how many people are using those filesystems, so we cannot determine if it's worth the effort to implement. Some of the data is not as useful for making decisions, but is still useful for us to know. For example, have you ever wondered how many Panel instances there are? How many servers are being ran across all of those instances? How many users are using the Panel? How many of those users are admins? How many servers are using a specific egg? How many servers are using a specific nest? All of these questions can be answered by the data we collect, and can help us and the community better understand how the software is being used. If you have any questions about the data we collect, please feel free to reach out to us on Discord. Our goal is to be as transparent as possible, and we want to make sure that the community understands what we are doing and why. ### Enabling Telemetry [#enabling-telemetry] Telemetry is enabled by default, if you want to enable it after disabling it, edit your `.env` file and either remove the `PTERODACTYL_TELEMETRY_ENABLED` line, or set it to `true`. ```text PTERODACTYL_TELEMETRY_ENABLED=true ``` You may also use the `php artisan p:environment:setup` command to enable telemetry, optionally with the `--telemetry` flag for a non-interactive setup. ### Disabling Telemetry [#disabling-telemetry] To disable telemetry, edit your `.env` file and set `PTERODACTYL_TELEMETRY_ENABLED` to `false`. ```text PTERODACTYL_TELEMETRY_ENABLED=false ``` You may also use the `php artisan p:environment:setup` command to disable telemetry, optionally with the `--telemetry=false` flag for a non-interactive setup. # Getting Started Pterodactyl Panel is designed to run on your own web server. You will need to have root access to your server in order to run and use this panel. You are expected to understand how to read documentation to use this Panel. We have spent many hours detailing how to install or upgrade our software; take some time and read rather than copy and pasting and then complaining when things do not work. This panel does not exist as a drag-and-drop service to run your servers. It is a highly complex system requiring multiple dependencies and administrators willing to spend some time learning how to use it. **If you expect to be able to install this with no understanding of basic linux system administration you should stop and turn around now.** [WISP](https://wisp.gg) is a Pterodactyl powered SaaS suitable for enterprise and personal use. Offering all the features without the setup hassle, and fully compatible with Pterodactyl eggs. Comparable to MultiCraft or TCAdmin while offering new and unique features. Click here to [learn more](https://wisp.gg/features). ## Picking a Server OS [#picking-a-server-os] Pterodactyl runs on a wide range of operating systems, so pick whichever you are most comfortable using. Running Wings under LXC or OpenVZ containers is possible as long as the virtualization layer supports Docker. Most modern providers with LXC/OpenVZ 7+ and nested virtualization enabled will work without issues. If you run into problems, ensure that your provider has enabled the necessary kernel features for Docker. | Operating System | Version | Supported | Notes | | ---------------------------------- | ------- | :-------: | ----------------------------------------------------------- | | **Ubuntu** | 22.04 | ✅ | Requires additional repositories for PHP | | | 24.04 | ✅ | MariaDB can be installed without the repo setup script. | | **RHEL / Rocky Linux / AlmaLinux** | 8 | ✅ | Extra repos are required. | | | 9 | ✅ | | | **Debian** | 11 | ✅ | [Debian Dependencies](/v1/guides/panel-installation/debian) | | | 12 | ✅ | [Debian Dependencies](/v1/guides/panel-installation/debian) | | | 13 | ✅ | [Debian Dependencies](/v1/guides/panel-installation/debian) | ## Dependencies [#dependencies] * PHP `8.2` or `8.3` (recommended) with the following extensions: `cli`, `openssl`, `gd`, `mysql`, `PDO`, `mbstring`, `tokenizer`, `bcmath`, `xml` or `dom`, `curl`, `zip`, and `fpm` if you are planning to use NGINX. * MySQL `5.7.22` and higher (MySQL `8` recommended) **or** MariaDB `10.2` and higher. * Redis (`redis-server`) * A webserver (Apache, NGINX, Caddy, etc.) * `curl` * `tar` * `unzip` * `git` * `composer` v2 ### Example Dependency Installation [#example-dependency-installation] The commands below are simply an example of how you might install these dependencies. Please consult with your operating system's package manager to determine the correct packages to install. ```bash # Add "add-apt-repository" command apt -y install software-properties-common curl apt-transport-https ca-certificates gnupg # Add additional repositories for PHP (Ubuntu 22.04) LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php # Add Redis official APT repository curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list # Update repositories list apt update # Install Dependencies apt -y install php8.3 php8.3-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip} mariadb-server nginx tar unzip git redis-server ``` ### Installing Composer [#installing-composer] Composer is a dependency manager for PHP that allows us to ship everything you'll need code wise to operate the Panel. You'll need composer installed before continuing in this process. ```bash curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer ``` ## Download Files [#download-files] The first step in this process is to create the folder where the panel will live and then move ourselves into that newly created folder. Below is an example of how to perform this operation. ```bash mkdir -p /var/www/pterodactyl cd /var/www/pterodactyl ``` Once you have created a new directory for the Panel and moved into it you'll need to download the Panel files. This is as simple as using `curl` to download our pre-packaged content. Once it is downloaded you'll need to unpack the archive and then set the correct permissions on the `storage/` and `bootstrap/cache/` directories. These directories allow us to store files as well as keep a speedy cache available to reduce load times. ```bash curl -Lo panel.tar.gz https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz tar -xzvf panel.tar.gz chmod -R 755 storage/* bootstrap/cache/ ``` ## Installation [#installation] Now that all of the files have been downloaded we need to configure some core aspects of the Panel. You will need a database setup and a user with the correct permissions created for that database before continuing any further. See below to create a user and database for your Pterodactyl panel quickly. To find more detailed information please have a look at [Setting up MySQL](/v1/guides/tutorials/mysql-setup). ```sql # If using MariaDB (v11.0.0+) (This is the default when installing Pterodactyl by following the documentation.) mariadb -u root -p # If using MySQL mysql -u root -p ``` ```sql # Remember to change 'yourPassword' below to be a unique password CREATE USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY 'yourPassword'; CREATE DATABASE panel; GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'127.0.0.1' WITH GRANT OPTION; exit ``` First we will copy over our default environment settings file, install core dependencies, and then generate a new application encryption key. ```bash cp .env.example .env COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader # Only run the command below if you are installing this Panel for # the first time and do not have any Pterodactyl Panel data in the database. php artisan key:generate --force ``` Back up your encryption key (`APP_KEY` in the `.env` file). It is used as an encryption key for all data that needs to be stored securely (e.g. API keys). Store it somewhere safe - not just on your server. If you lose it, all encrypted data is irrecoverable, even with database backups. To grab your `APP_KEY`, open a terminal and run the following in your panel directory: ```bash grep APP_KEY /var/www/pterodactyl/.env ``` You should see something like: ```text APP_KEY=base64:YOUR_LONG_RANDOM_STRING ``` Copy that entire line and save it somewhere secure: * A password manager * An encrypted file on your local machine * A secure USB drive * A trusted cloud vault Do not keep it only on the server. If you lose this key, your encrypted data is permanently unrecoverable. ### Environment Configuration [#environment-configuration] Pterodactyl's core environment is easily configured using a few different CLI commands built into the app. This step will cover setting up things such as sessions, caching, database credentials, and email sending. ```bash php artisan p:environment:setup php artisan p:environment:database # To use PHP's internal mail sending (not recommended), select "mail". To use a # custom SMTP server, select "smtp". php artisan p:environment:mail ``` ### Database Setup [#database-setup] Now we need to setup all of the base data for the Panel in the database you created earlier. **The command below may take some time to run depending on your machine. Please *DO NOT* exit the process until it is completed!** This command will setup the database tables and then add all of the Nests & Eggs that power Pterodactyl. ```bash php artisan migrate --seed --force ``` ### Add The First User [#add-the-first-user] You'll then need to create an administrative user so that you can log into the panel. To do so, run the command below. At this time passwords **must** meet the following requirements: 8 characters, mixed case, at least one number. ```bash php artisan p:user:make ``` ### Set Permissions [#set-permissions] The last step in the installation process is to set the correct permissions on the Panel files so that the webserver can use them correctly. ```bash # If using NGINX, Apache or Caddy (not on RHEL / Rocky Linux / AlmaLinux) chown -R www-data:www-data /var/www/pterodactyl/* # If using NGINX on RHEL / Rocky Linux / AlmaLinux chown -R nginx:nginx /var/www/pterodactyl/* # If using Apache on RHEL / Rocky Linux / AlmaLinux chown -R apache:apache /var/www/pterodactyl/* ``` ## Queue Listeners [#queue-listeners] We make use of queues to make the application faster and handle sending emails and other actions in the background. You will need to setup the queue worker for these actions to be processed. ### Crontab Configuration [#crontab-configuration] The first thing we need to do is create a new cronjob that runs every minute to process specific Pterodactyl tasks, such as session cleanup and sending scheduled tasks to daemons. You'll want to open your crontab using `sudo crontab -e` and then paste the line below. ```bash * * * * * php /var/www/pterodactyl/artisan schedule:run >> /dev/null 2>&1 ``` ### Create Queue Worker [#create-queue-worker] Next you need to create a new systemd worker to keep our queue process running in the background. This queue is responsible for sending emails and handling many other background tasks for Pterodactyl. Create a file called `pteroq.service` in `/etc/systemd/system` with the contents below. ```text # Pterodactyl Queue Worker File # ---------------------------------- [Unit] Description=Pterodactyl Queue Worker After=redis-server.service [Service] # On some systems the user and group might be different. # Some systems use `apache` or `nginx` as the user and group. User=www-data Group=www-data Restart=always ExecStart=/usr/bin/php /var/www/pterodactyl/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3 StartLimitInterval=180 StartLimitBurst=30 RestartSec=5s [Install] WantedBy=multi-user.target ``` If you are using RHEL, Rocky Linux, or AlmaLinux, you will need to replace `redis-server.service` with `redis.service` at the `After=` line in order to ensure `redis` starts before the queue worker. If you are not using `redis` for anything you should remove the `After=` line, otherwise you will encounter errors when the service starts. If you are using redis for your system, you will want to make sure to enable that it will start on boot. You can do that by running the following command: ```bash sudo systemctl enable --now redis-server ``` Finally, enable the service and set it to boot on machine start. ```bash sudo systemctl enable --now pteroq.service ``` ### Telemetry [#telemetry] Since 1.11, Pterodactyl will collect anonymous telemetry to help us better understand how the software is being used. To learn more about this feature and to opt-out, please see our [Telemetry](/v1/panel/additional-configuration#telemetry) documentation. Make sure to continue with the rest of the installation process. # Troubleshooting ## Reading Error Logs [#reading-error-logs] If you ever encounter an unexpected error with the Panel the first thing you will likely be asked for is the logs. To retrieve these, simply execute the command below which will output the last 100 lines of the Panel's log file. ```bash tail -n 100 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log ``` ### Parsing the Error [#parsing-the-error] When you run the command above, you'll probably be hit with a huge wall of text that might scare you. Fear not, this is simply a stacktrace leading to the cause of the error, and you can actually ignore almost all of it when looking for the cause of the error. Lets take a look at some example output below, which has been truncated to make this easier to follow with. ``` #70 /srv/www/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(116): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter(Object(Illuminate\Http\Request)) #71 /srv/www/public/index.php(53): Illuminate\Foundation\Http\Kernel->handle(Object(Illuminate\Http\Request)) #72 {main} [2018-07-19 00:50:24] local.ERROR: ErrorException: file_put_contents(/srv/www/storage/framework/views/c9c05d1357df1ce4ec8fc5df78c16c493b0d4f48.php): failed to open stream: Permission denied in /srv/www/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php:122 Stack trace: #0 [internal function]: Illuminate\Foundation\Bootstrap\HandleExceptions->handleError(2, 'file_put_conten...', '/srv/www/vendor...', 122, Array) #1 /srv/www/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php(122): file_put_contents('/srv/www/storag...', 's...', 0) #2 /srv/www/vendor/laravel/framework/src/Illuminate/View/Compilers/BladeCompiler.php(122): Illuminate\Filesystem\Filesystem->put('/srv/www/storag...', 's...') #3 /srv/www/vendor/laravel/framework/src/Illuminate/View/Engines/CompilerEngine.php(51): Illuminate\View\Compilers\BladeCompiler->compile('/srv/www/resour...') #4 /srv/www/vendor/laravel/framework/src/Illuminate/View/View.php(142): Illuminate\View\Engines\CompilerEngine->get('/srv/www/resour...', Array) #5 /srv/www/vendor/laravel/framework/src/Illuminate/View/View.php(125): Illuminate\View\View->getContents() ``` The first thing you'll want to do is follow the chain of numbers *up* until you find `#0`, this will be the function that triggered the exception. Right above line 0 you will see a line that has the date and time in brackets, `[2018-07-19 00:50:24]` above for example. This line will be the human readable exception that you can use to understand what went wrong. ### Understanding the Error [#understanding-the-error] In the example above we can see that the actual error was: ``` local.ERROR: ErrorException: file_put_contents(...): failed to open stream: Permission denied in /srv/www/vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php:122 ``` From this error we can determine that there was an error performing a [file\_put\_contents()](http://php.net/manual/en/function.file-put-contents.php) call, and the error was that we couldn't open the file because permissions were denied. Its okay if you don't understand the error at all, but it does help you get faster support if you're able to provide these logs, and at least find the source of the error. Sometimes the errors are pretty straightforward and will tell you exactly what went wrong, such as a `ConnectionException` being thrown when the Panel can't connect to the Daemon. ### Utilizing GREP [#utilizing-grep] If you're trying to go through a bunch of errors quickly, you can use the command below which will limit the results returned to only be the actual error lines, without all of the stack traces. ```bash tail -n 1000 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | grep "\[$(date +%Y)" ``` ## Cannot Connect to Server Errors [#cannot-connect-to-server-errors] ### Basic Debugging Steps [#basic-debugging-steps] * Check that Wings is running, and not reporting errors. Use `systemctl status wings` to check the current status of the process. * Check your browser's console by pressing `Ctrl + Shift + J` (in Chrome) or `Cmd + Alt + I` (in Safari). If there is a red error in it, chances are that it will narrow down the potential problem. * Make sure Wings is properly installed and the active configuration matches the configuration shown under `Admin -> Node -> Configuration` in the Panel. * Check that the Wings ports are open on your firewall. Wings uses ports `8080` or `8443` for HTTP(s) traffic, and `2022` for SFTP traffic. * Ensure you have AdBlock disabled or whitelisted for your Panel and Wings domains. * Check that the Panel can reach Wings using the domain that is configured on the Panel. Run `curl https://domain.com:8080` on the Panel server and ensure that it can successfully connect to Wings. * Ensure that you are using the correct HTTP scheme for your Panel and Wings. If the Panel is running over HTTPS Wings will also need to be running on HTTPS. * If using HTTPS for Wings, make sure that the certificates have not expired. ### More Advanced Debugging Steps [#more-advanced-debugging-steps] * Stop Wings and run `wings --debug` to see if there are any errors being output. If so, try resolving them manually, or reach out on [Discord](https://discord.gg/pterodactyl) for more assistance. * Check your DNS and ensure that the response you receive is the one you expect using a tool such as `nslookup` or `dig`. * If you use CloudFlare make sure that the orange cloud is disabled for your Wings or Panel `A` records. * Make sure when using Wings behind a firewall — pfSense, OpenSwitch, etc. — that the correct NAT settings to access the Wing's ports from the outside network are setup. * If nothing is working so far, check your own DNS settings and consider switching DNS servers. * When running the Panel and Wings on one server it can sometimes help if to add an entry in `/etc/hosts` that directs the public IP back to the server. Sometimes the reverse path is also needed, so you may need to add an entry to your servers `/etc/hosts` file that points the Panel's domain to the correct IP. * When running Wings and the Panel on separate VM's using the same adapter make sure the VM's can connect to each other. Promiscuous mode might be needed. ## Invalid MAC Exception [#invalid-mac-exception] This error should never happen if you correctly follow our installation and upgrade guides. The only time we have ever seen this error occur is when you blindly restore the Panel database from a backup and try to use a fresh installation of the Panel. When restoring backups you should *always* restore the `.env` file! Sometimes when using the Panel you'll unexpectedly encounter a broken page, and upon checking the logs you'll see an exception mentioning an invalid MAC when decrypting. This error is caused by mismatched `APP_KEY`s in your `.env` file when the data was encrypted versus decrypted. If you are seeing this error the only solution is to restore the `APP_KEY` from your `.env` file. If you have lost that original key there is no way to recover the lost data. ## SELinux Issues [#selinux-issues] On systems with SELinux installed you might encounter unexpected errors when running redis or attempting to connect to the daemon to perform actions. These issues can generally be resolved by executing the commands below to allow these programs to work with SELinux. ### Redis Permissions Errors [#redis-permissions-errors] ```bash audit2allow -a -M redis_t semodule -i redis_t.pp ``` ### Wings Connection Errors [#wings-connection-errors] ```bash audit2allow -a -M http_port_t semodule -i http_port_t.pp ``` ## Containers don't have internet? Probably a DNS issue! [#containers-dont-have-internet-probably-a-dns-issue] Now that Wings has run successfully and you have gotten the green heart on your Nodes page, the wings config at '/etc/pterodactyl/config.yml' will have new values. One of those values is DNS, which by default will be 1.1.1.1 and 1.0.0.1 If you are using a host that blocks Cloudflare DNS, you will have to use different DNS Servers; typically the same ones your host system is using. You can view what DNS Servers your host uses through a number of ways depending on how your operating system handles networking. If one of these doesn't work, try another one. ```bash # Network Manager (This will show both your IPV4 DNS and IPV6 DNS Servers in case you want to add the IPV6 DNS Server(s) from your host to your Wings Config as well. nmcli -g ip4.dns,ip6.dns dev show # Resolve-CTL (Newer Versions of Ubuntu) resolvectl status # Raw file locations that may have your host system's DNS Servers for various distributions /etc/resolv.conf /etc/network/interfaces ``` If this returns different DNS Servers than 1.1.1.1 and 1.0.0.1 you'll need to edit the wings 'config.yml' file to use the DNS servers that were returned from the command. If you see output that looks like an IPV6 address in addition to your IPV4 DNS Servers, make sure you put that in the IPV6 section and not the IPV4 section. To be clear, if you have to use different DNS Servers than the default, make sure to REMOVE 1.1.1.1 and 1.0.0.1 from the wings config; don't just add the new servers, replace the old servers. ## Schedule Troubleshooting [#schedule-troubleshooting] * Check logs from your queue manager `journalctl -xeu pteroq` * Restart pteroq `systemctl restart pteroq` * Clear schedule cache `php /var/www/pterodactyl/artisan schedule:clear-cache` * Check your php version `php -v` - [this page](/v1/panel/updating#panel-version-requirements) will tell you what versions of php are supported by what versions of the panel * Check your crontab syntax using [https://crontab.guru](https://crontab.guru) - make sure it's what you intended * Verify the problem is with the schedule and not with the tasks you have set up (Set the first task in your schedule to something you know prints a message in the console, ie. run `say test` in the console for a Minecraft server, if the text "test" shows up in the console successfully, set the first task to `say test` so you know if it runs * Are your tasks off by a bit? Make sure you on the latest version of the panel? In version 1.11.5 there was a fix for schedules running at the wrong time. Alternatively, you may have the wrong timezone set. Make sure your timezones all match. * System Timezone `timedatectl` * Panel Timezone `nano /var/www/pterodactyl/.env` * Wings Timezone (Passed to containers as the TZ environmental variable, unrelated to schedules but while you're checking timezones you may as well set this too) `nano /etc/pterodactyl/config.yml` * Check your database where schedules are stored - MariaDB by default * `systemctl status mariadb` - if it's not active, `journalctl -xeu mariadb` * Check queue handler - Redis by default * `systemctl status redis` - if it's not active, `journalctl -xeu redis` (On some distributions the service will be named `redis-server` instead) * Check for panel errors `tail -n 150 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).log | nc pteropaste.com 99` ## FirewallD issues [#firewalld-issues] If you are on a RHEL/CentOS server with `firewalld` installed you may have broken DNS. ```bash firewall-cmd --permanent --zone=trusted --change-interface=pterodactyl0 firewall-cmd --reload ``` Restart `docker` and `wings` after running these to be sure the rules are applied. # Updating the Panel This documentation covers the process for updating within the `1.x` series of releases. This means updating from — for example — `1.11.x` to `1.12.x`. Do not use these steps to upgrade to 2.0. They unpack the new version on top of your current files, which leaves a 2.0 Panel offline. For the same reason, do not run `php artisan p:upgrade` without `--release`. Once 2.0 is released, it will download 2.0. To upgrade to 2.0, follow [Upgrading From 1.x](/v2/upgrading/upgrading-from-v1). ## Panel Version Requirements [#panel-version-requirements] Each version of Pterodactyl Panel also has a corresponding minimum version of Wings that is required for it to run. Please see the chart below for how these versions line up. In most cases your base Wings version should match that of your Panel. | Panel Version | Wings Version | Supported | PHP Versions | | ------------- | ------------- | --------- | ----------------- | | 1.0.x | 1.0.x | | 7.3, 7.4 | | 1.1.x | 1.1.x | | 7.3, 7.4 | | 1.2.x | 1.2.x | | 7.3, 7.4 | | 1.3.x | 1.3.x | | 7.4, 8.0 | | 1.4.x | 1.4.x | | 7.4, 8.0 | | 1.5.x | 1.4.x | | 7.4, 8.0 | | 1.6.x | 1.4.x | | 7.4, 8.0 | | 1.7.x | 1.5.x | | 7.4, 8.0 | | 1.8.x | 1.6.x | | 7.4, 8.0, 8.1 | | 1.9.x | 1.6.x | | 7.4, 8.0, 8.1 | | 1.10.x | 1.7.x | | 7.4, 8.0, 8.1 | | 1.11.x | 1.11.x | | ~~8.1~~, 8.2, 8.3 | | **1.12.x** | **1.12.x** | ✅ | 8.2, **8.3** | There are no 1.8.x, 1.9.x, or 1.10.x releases of Wings. ## Update Dependencies [#update-dependencies] * PHP `8.2`, or `8.3` (recommended) * Composer `2.X` **Before continuing**, please ensure that your system and web server configuration has been upgraded to at least PHP 8.2 by running `php -v` and Composer 2 by running `composer --version`. You should see an output similar to the result below. If you do not see at least PHP 8.2 and Composer 2, you will need to upgrade by following our [PHP Upgrade Guide](/v1/guides/configuration/php-upgrade) and return to this documentation afterward. ```shell vagrant@pterodactyl:~/app$ php -v PHP 8.3.30 (cli) (built: Dec 21 2022 10:32:13) (NTS) Copyright (c) The PHP Group Zend Engine v4.3.30, Copyright (c) Zend Technologies with Zend OPcache v8.3.30, Copyright (c), by Zend Technologies vagrant@pterodactyl:~/app$ composer --version Composer version 2.8.12 2025-09-19 13:41:59 ``` ## Upgrade Steps [#upgrade-steps] Follow the steps below to upgrade your Panel to the latest version. ### Enter Maintenance Mode [#enter-maintenance-mode] Whenever you are performing an update you should be sure to place your Panel into maintenance mode. This will prevent users from encountering unexpected errors and ensure everything can be updated before users encounter potentially new features. ```bash cd /var/www/pterodactyl php artisan down ``` ### Download the Update [#download-the-update] The first step in the update process is to download the new panel files from GitHub. The command below will download the release archive for the latest 1.x release (1.15.1 at the time of writing) and will automatically unpack the archive into your current folder. Do not replace the version with `latest`: once 2.0 is published, `latest` points at 2.0. ```bash curl -L https://github.com/pterodactyl/panel/releases/download/v1.15.1/panel.tar.gz | tar -xzv ``` Once all of the files are downloaded we need to set the correct permissions on the cache and storage directories to avoid any webserver related errors. ```bash chmod -R 755 storage/* bootstrap/cache ``` ### Update Dependencies [#update-dependencies-1] After you've downloaded all of the new files you will need to upgrade the core components of the panel. To do this, simply run the commands below and follow any prompts. ```bash composer install --no-dev --optimize-autoloader ``` ### Clear Compiled Template Cache [#clear-compiled-template-cache] You'll also want to clear the compiled template cache to ensure that new and modified templates show up correctly for users. ```bash php artisan view:clear php artisan config:clear ``` ### Database Updates [#database-updates] You'll also need to update your database schema for the newest version of Pterodactyl. Running the command below will update the schema and ensure the default eggs we ship are up to date (and add any new ones we might have). Just remember, *never edit core eggs we ship*! They will be overwritten by this update process. ```bash php artisan migrate --seed --force ``` ### Set Permissions [#set-permissions] The last step is to set the proper owner of the files to be the user that runs your webserver. In most cases this is `www-data` but can vary from system to system — sometimes being `nginx`, `caddy`, `apache`, or even `nobody`. ```bash # If using NGINX, Apache or Caddy (not on RHEL / Rocky Linux / AlmaLinux) chown -R www-data:www-data /var/www/pterodactyl/* # If using NGINX on RHEL / Rocky Linux / AlmaLinux chown -R nginx:nginx /var/www/pterodactyl/* # If using Apache on RHEL / Rocky Linux / AlmaLinux chown -R apache:apache /var/www/pterodactyl/* ``` ### Restarting Queue Workers [#restarting-queue-workers] After *every* update you should restart the queue worker to ensure that the new code is loaded in and used. ```bash php artisan queue:restart ``` ### Exit Maintenance Mode [#exit-maintenance-mode] Now that everything has been updated you need to exit maintenance mode so that the Panel can resume accepting connections. ```bash php artisan up ``` ### Telemetry [#telemetry] Since 1.11, Pterodactyl will collect anonymous telemetry to help us better understand how the software is being used. To learn more about this feature and to opt-out, please see our [Telemetry](/v1/panel/additional-configuration#telemetry) documentation. Remember to continue with the rest of the upgrade. # Webserver Configuration When using the SSL configuration you MUST create SSL certificates, otherwise your webserver will fail to start. See the [Creating SSL Certificates](/v1/guides/tutorials/ssl-certificates) documentation page to learn how to create these certificates before continuing. If you are using Caddy With Automatic SSL, you do not have to create SSL certificates manually, Caddy will take care of it automatically. First, remove the default NGINX configuration. ```bash rm /etc/nginx/sites-enabled/default ``` Now, you should paste the contents of the file below, replacing `` with your domain name being used in a file called `pterodactyl.conf` and place the file in `/etc/nginx/sites-available/`, or — if on RHEL, Rocky Linux, or AlmaLinux, `/etc/nginx/conf.d/`. ```nginx title="pterodactyl.conf" {4,11,26-27} server { # Replace the example with your domain name or IP address listen 80; server_name ; return 301 https://$server_name$request_uri; } server { # Replace the example with your domain name or IP address listen 443 ssl http2; server_name ; root /var/www/pterodactyl/public; index index.php; access_log /var/log/nginx/pterodactyl.app-access.log; error_log /var/log/nginx/pterodactyl.app-error.log error; # allow larger file uploads and longer script runtimes client_max_body_size 100m; client_body_timeout 120s; sendfile off; # SSL Configuration - Replace the example with your domain ssl_certificate /etc/letsencrypt/live//fullchain.pem; ssl_certificate_key /etc/letsencrypt/live//privkey.pem; ssl_session_cache shared:SSL:10m; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; ssl_prefer_server_ciphers on; # See https://hstspreload.org/ before uncommenting the line below. # add_header Strict-Transport-Security "max-age=15768000; preload;"; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header X-Robots-Tag none; add_header Content-Security-Policy "frame-ancestors 'self'"; add_header X-Frame-Options DENY; add_header Referrer-Policy same-origin; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M"; 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 /etc/nginx/fastcgi_params; } location ~ /\.ht { deny all; } } ``` ### Enabling Configuration [#enabling-configuration] The final step is to enable your NGINX configuration and restart it. ```bash # You do not need to symlink this file if you are using RHEL, Rocky Linux, or AlmaLinux. sudo ln -s /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/pterodactyl.conf # You need to restart nginx regardless of OS. sudo systemctl restart nginx ``` First, remove the default NGINX configuration. ```bash rm /etc/nginx/sites-enabled/default ``` Now, you should paste the contents of the file below, replacing `` with your domain name being used in a file called `pterodactyl.conf` and place the file in `/etc/nginx/sites-available/`, or — if on RHEL, Rocky Linux, or AlmaLinux, `/etc/nginx/conf.d/`. ```nginx title="pterodactyl.conf" {4} server { # Replace the example with your domain name or IP address listen 80; server_name ; root /var/www/pterodactyl/public; index index.html index.htm index.php; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } access_log off; error_log /var/log/nginx/pterodactyl.app-error.log error; # allow larger file uploads and longer script runtimes client_max_body_size 100m; client_body_timeout 120s; sendfile off; location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param PHP_VALUE "upload_max_filesize = 100M \n post_max_size=100M"; 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; } location ~ /\.ht { deny all; } } ``` ### Enabling Configuration [#enabling-configuration-1] The final step is to enable your NGINX configuration and restart it. ```bash # You do not need to symlink this file if you are using RHEL, Rocky Linux, or AlmaLinux. sudo ln -s /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/pterodactyl.conf # You need to restart nginx regardless of OS. sudo systemctl restart nginx ``` First, remove the default Apache configuration. ```bash a2dissite 000-default.conf ``` Now, you should paste the contents of the file below, replacing `` with your domain name being used in a file called `pterodactyl.conf` and place the file in `/etc/apache2/sites-available`, or — if on RHEL, Rocky Linux, or AlmaLinux, `/etc/httpd/conf.d/`. Note: When using Apache, make sure you have the `libapache2-mod-php8.3` package installed or else PHP will not display on your webserver. ```apache title="pterodactyl.conf" {3,12,26-27} # Replace the example with your domain name or IP address ServerName RewriteEngine On RewriteCond %{HTTPS} !=on RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L] # Replace the example with your domain name or IP address ServerName DocumentRoot "/var/www/pterodactyl/public" AllowEncodedSlashes On php_value upload_max_filesize 100M php_value post_max_size 100M Require all granted AllowOverride all SSLEngine on SSLCertificateFile /etc/letsencrypt/live//fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live//privkey.pem ``` ### Enabling Configuration [#enabling-configuration-2] Once you've created the file above, simply run the commands below. If you are on RHEL, Rocky Linux, or AlmaLinux you do not need to run the commands below! You only need to run `systemctl restart httpd`. ```bash # You do not need to run any of these commands on RHEL, Rocky Linux, or AlmaLinux sudo ln -s /etc/apache2/sites-available/pterodactyl.conf /etc/apache2/sites-enabled/pterodactyl.conf sudo a2enmod rewrite sudo a2enmod ssl sudo systemctl restart apache2 ``` First, remove the default Apache configuration. ```bash a2dissite 000-default.conf ``` Now, you should paste the contents of the file below, replacing `` with your domain name being used in a file called `pterodactyl.conf` and place the file in `/etc/apache2/sites-available`, or — if on RHEL, Rocky Linux, or AlmaLinux, `/etc/httpd/conf.d/`. Note: When using Apache, make sure you have the `libapache2-mod-php8.3` package installed or else PHP will not display on your webserver. ```apache title="pterodactyl.conf" {3} # Replace the example with your domain name or IP address ServerName DocumentRoot "/var/www/pterodactyl/public" AllowEncodedSlashes On php_value upload_max_filesize 100M php_value post_max_size 100M AllowOverride all Require all granted ``` ### Enabling Configuration [#enabling-configuration-3] Once you've created the file above, simply run the commands below. If you are on RHEL, Rocky Linux, or AlmaLinux you do not need to run the commands below! You only need to run `systemctl restart httpd`. ```bash # You do not need to run any of these commands on RHEL, Rocky Linux, or AlmaLinux sudo ln -s /etc/apache2/sites-available/pterodactyl.conf /etc/apache2/sites-enabled/pterodactyl.conf sudo a2enmod rewrite sudo systemctl restart apache2 ``` Before adding our custom configuration, let's remove the default one. You can do it either by deleting the contents of config file or by deleting the config file completely and than creating a new one from scratch. The config file path is `/etc/caddy/Caddyfile`. To delete the config file completely, run the following command: ```shell rm /etc/caddy/Caddyfile ``` Then continue with an editor of your choice to write the config. You should paste the contents of the file below, replacing `` with your domain name. ```shell title="Caddyfile" {10} { servers :443 { timeouts { read_body 120s } } } # Replace the example with your domain name or IP address { root * /var/www/pterodactyl/public file_server php_fastcgi unix//run/php/php8.3-fpm.sock { root /var/www/pterodactyl/public index index.php env PHP_VALUE "upload_max_filesize = 100M post_max_size = 100M" env HTTP_PROXY "" env HTTPS "on" read_timeout 300s dial_timeout 300s write_timeout 300s } header Strict-Transport-Security "max-age=16768000; preload;" header X-Content-Type-Options "nosniff" header X-XSS-Protection "1; mode=block;" header X-Robots-Tag "none" header Content-Security-Policy "frame-ancestors 'self'" header X-Frame-Options "DENY" header Referrer-Policy "same-origin" request_body { max_size 100m } respond /.ht* 403 log { output file /var/log/caddy/pterodactyl.log { roll_size 100MiB roll_keep_for 7d } level INFO } } ``` If you are using Cloudflare DNS in proxy mode, refer to [this tutorial](/v1/guides/tutorials/ssl-certificates#method-3-caddy-using-cloudflare-api), to see how to configure Caddy to use DNS challenge for obtaining SSL certificates. ### Enabling Configuration [#enabling-configuration-4] The final step is to restart Caddy. ```bash systemctl restart caddy ``` Before adding our custom configuration, let's remove the default one. You can do it either by deleting the contents of config file or by deleting the config file completely and than creating a new one from scratch. The config file path is `/etc/caddy/Caddyfile`. To delete the config file completely, run the following command: ```shell rm /etc/caddy/Caddyfile ``` Then continue with an editor of your choice to write the config. You should paste the contents of the file below, replacing `` with your domain name. The only two differences are that we have suffixed the `` with `:80` and in the global config at `servers` directive, we have changed the port from `:443` to `:80`. ```shell title="Caddyfile" {10} { servers :80 { timeouts { read_body 120s } } } # Replace the example with your domain name or IP address :80 { root * /var/www/pterodactyl/public file_server php_fastcgi unix//run/php/php8.3-fpm.sock { root /var/www/pterodactyl/public index index.php env PHP_VALUE "upload_max_filesize = 100M post_max_size = 100M" env HTTP_PROXY "" # env HTTPS "on" # IMPORTANT: this is commented out, to disable HTTPS read_timeout 300s dial_timeout 300s write_timeout 300s } header X-Content-Type-Options "nosniff" header X-XSS-Protection "1; mode=block;" header X-Robots-Tag "none" header Content-Security-Policy "frame-ancestors 'self'" header X-Frame-Options "DENY" header Referrer-Policy "same-origin" request_body { max_size 100m } respond /.ht* 403 log { output file /var/log/caddy/pterodactyl.log { roll_size 100MiB roll_keep_for 7d } level INFO } } ``` ### Enabling Configuration [#enabling-configuration-5] The final step is to restart Caddy. ```bash systemctl restart caddy ``` # About ## Core Project Team [#core-project-team] | Name | GitHub | Primary Role | | ------------- | ---------------------------------------------------------- | ------------------ | | Robert Dennis | [@robertdrakedennis](https://github.com/robertdrakedennis) | Project Maintainer | | Sky Mulley | [@SkyMulley](https://github.com/SkyMulley) | Project Maintainer | Members of the project team have a red username in our Discord server. ## Community Team [#community-team] Pterodactyl owes much of its success to our community support team. Its members have a yellow username in our Discord server. ## Sponsors [#sponsors] These companies help fund Pterodactyl's development. [Interested in becoming a sponsor?](https://github.com/sponsors/pterodactyl) | Company | About | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [**Buildurly**](https://buildurly.com/) | Buildurly is a hardware procurement company. They deliver tailored, enterprise-grade hardware solutions designed around your unique needs. From sourcing to delivery, Buildurly's white-glove service ensures a seamless, worry-free, professional experience. | | [**BuiltByBit**](https://builtbybit.com/) | BuiltByBit is a marketplace for game server and community assets. Creators sell plugins, setups, builds, bots and websites for Minecraft, Roblox, Hytale, Garry's Mod and Discord. | | [**Hosturly**](https://hosturly.com/) | Hosturly is an enterprise hosting provider. They provide cost-effective, high-performance, and reliable services, including VPS, Web, Dedicated, and Colocation. | | [**indifferent broccoli**](https://indifferentbroccoli.com/) | indifferent broccoli is a game server hosting and rental company. With them, you get top-notch computer power for your gaming sessions. They destroy lag, latency, and complexity--letting you focus on the fun stuff. | | [**Infraly, LLC**](https://infraly.co/) | Infraly is an infrastructure company powering the next generation of online services. Through their brands, Infraly delivers cutting-edge solutions across multiple markets. Their vertically integrated approach provides unmatched performance, scalability, and reliability, giving our customers full control. | | [**MineStrator**](https://minestrator.com/) | MineStrator is a game server hosting provider. Looking for the most high-end French hosting company for your Minecraft server? More than 24,000 members on our Discord trust us. Give us a try! | | [**Physgun**](https://physgun.com/) | Physgun is a game server hosting provider. Most providers rent rack space and rebrand a panel. At Physgun, they engineer the performance, write the features, and staff the support. Physgun truly is game hosting perfected! | | [**WISP**](https://wisp.gg/) | WISP is an industry-leading SaaS platform for game server management, designed for hosting companies, gaming organizations, and enthusiasts. WISP combines modern, intuitive interfaces with powerful tools, making server deployment and administration seamless, scalable, and efficient. | ## License [#license] Copyright © 2015 Pterodactyl®. Code released under the [MIT License](https://github.com/pterodactyl/panel/blob/develop/LICENSE.md). ## Release Signing [#release-signing] Previously, releases were signed by a GPG key. All recent releases are now signed using an SSH signing key. This key is used to sign release tags and commits created by Matthew Penner. This key was first used to sign `v1.10.2` for the Panel and `v1.7.1` for Wings and has been used ever since. ```text ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKL873MsP1OFfffNC8n9WcVuOXOSW65/q26MIzib0K9k ``` # Community Standards Pterodactyl has grown from a community of tens in 2015 to a community of thousands in 2020. During that time there have been countless growing pains and community has changed in an innumerable number of ways. At our heart however, Pterodactyl continues to exist for one purpose: to be *the* platform for running your game servers. In order to keep true to that goal, and continue to foster one of the largest open-source game panel communities out there, we've adopted a simple set of guidelines for participating in this community. The goal of these guidelines is to foster an inclusive, welcoming environment for new users, and provide a space for the thousands of existing users, administrators, network owners, and hosting companies to co-exist. These rules and guidelines extend to all facets of the Pterodactyl Community, including but not limited to our Discord Server and all activities within the GitHub Organization. ## Community Guidelines [#community-guidelines] At the most basic level, these guidelines can be distilled down to: 1. Be a decent human. 2. Patience is a virtue. ### Be Mature [#be-mature] You are expected to be mature and control your behavior in a manner that adheres to basic human decency. If you are unable to do this you will be removed from the community. Personal attacks, spam (in any form), "doxxing", or otherwise acting out is not allowed. This community is fairly lax in regards to moderating language. However, the following are some examples of behavior that is absolutely *not* tolerated and for which you will be removed from the community. * Racist, sexist, homophobic, transphobic, or otherwise derogatory speech, images, insinuations, or any language whose sole purpose is to denigrate any individual, organization, or class of individual. * Threats of violence against any person, group, or organization including "doxxing" of these entities. * Pornographic or excessively violent content. ### Limit the Drama [#limit-the-drama] Discussion, including linking to or discussing sites or software, that exists to cast a negative image of other companies or users is not allowed. This includes calling out hosts using nulled software, attempting to elicit negative reactions towards services or websites, or otherwise stirring up drama. Assume someone is acting in good faith when responding to them. You don't have to agree with everyone, and you don't need to respond to everything. ### Be Patient [#be-patient] This is an open-source project. No members of the development team are paid in an official capacity to write, maintain, nor support this software. The following actions are discouraged in this community. * Repeatedly asking identical questions within the same channel (or across channels) within short periods of time. * It is expected that some questions will be missed. If it has been a reasonable amount of time and your question remains unanswered, you're welcome to re-post it. * Keep all support questions within the realm of the support channels. * Do not interrupt conversations in non-support channels solely to request that someone look in a support channel and help you. ### No Commercial Services [#no-commercial-services] Discussion of paid installation/upgrade services, modifications, or any other commercial offerings is strictly prohibited unless otherwise noted. This also includes reaching out to individuals via Direct Message and offering your services without provocation. Advertising commercial services within your username or display name on Discord is forbidden. [Sponsors](./about.mdx#sponsors) at the silver tier and higher are exempt from this rule. ### No Mention or Ping Spam [#no-mention-or-ping-spam] Please, do not direct message any administrative, development, or notable community members without first checking with them. Keep all support queries within the public support channels unless you have been directly asked to move it elsewhere. *But what if I am trying to respond back to someone?* That is fine! We only ask that you not mention people directly if they're not already involved in a discussion with you. # Introduction Pterodactyl is the 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 administrators and users. What more are you waiting for? Make game servers a first-class citizen on your platform today. ## Supported Games [#supported-games] We support a huge variety of games by utilizing Docker containers to isolate each instance, giving you the power to host your games across the world without having to bloat each physical machine with additional dependencies. Some of our core supported games include: * Minecraft — including Spigot, Sponge, Bungeecord, Waterfall, and more * Rust * Terraria * Teamspeak * Mumble * Team Fortress 2 * Counter-Strike: Global Offensive * Garry's Mod * ARK: Survival Evolved In addition to our standard nest of supported games, our community is constantly pushing the limits of this software and there are plenty more games available provided by the community. Some of these games include: * Factorio * San Andreas: MP * Pocketmine MP * Squad * FiveM * Xonotic * Discord ATLBot * [and many more...](https://pterodactyleggs.com) ## Responsible Disclosure [#responsible-disclosure] Pterodactyl is completely open-source, and as such completely open to independent users and auditors to browse our code base and hunt for security issues. If you come across anything that raises red flags for you, please do not hesitate to reach out directly to `support@pterodactyl.io`. We ask that you please be responsible when disclosing any security concerns and *do not* report them on our public facing bug tracker. # Terminology **Panel** — This refers to Pterodactyl Panel itself, and is what allows you to add additional nodes and servers to the system. **Node** — A node is a physical machine that runs an instance of Wings. **Wings** — The newer service written in Go that interfaces with Docker and the Panel to provide secure access for controlling servers via the Panel. **Server** — In this case, a server refers to a running instance that is created by the panel. These servers are created on nodes, and you can have multiple servers per node. **Docker** — Docker is a platform that lets you separate the application from your infrastructure into isolated, secure containers. **Docker Image** — A Docker image contains everything needed to run a containerized application. (e.g. Java for a Minecraft Server). **Container** — Each server will be running inside an isolated container to enforce hardware limitations (such as CPU and RAM) and avoid any interference between servers on one node. These are created by Docker. **Nest** — Each nest is usually used as a specific game or service, for example: Minecraft, Teamspeak or Terraria and can contain many eggs. **Egg** — Each egg is usually used to store the configuration of a specific type of game, for example: Vanilla, Spigot or Bungeecord for Minecraft. **Yolks** — A curated collection of core docker images that can be used with Pterodactyl's Egg system. ## Simple Setup Diagram [#simple-setup-diagram] Simple Setup Diagram ## Advanced Setup Diagram [#advanced-setup-diagram] It is also possible to install wings on the panel machine so it acts as panel and node machine at once. Advanced Setup Example # Additional Configuration These are advanced configurations for Wings. You risk breaking Wings and making containers unusable if you misconfigure something. Proceed only if you know what each configuration value does. You must apply all changes to your Wings `config.yml` file located at `/etc/pterodactyl` and restart wings. Verify your config file using [Yaml Lint](http://www.yamllint.com/) should you receive errors related to YAML parsing. ## Private Registries [#private-registries] You can use these settings to authenticate against (private) docker registries when pulling images. ### Available Keys [#available-keys] | Setting Key | Default Value | Notes | | ----------- | :-----------: | ----------------- | | name | null | Registry address | | username | null | Registry username | | password | null | Registry password | ### Example of usage [#example-of-usage] ```yml docker: registries: registry.example.com: username: "registryusername" password: "registrypassword" ``` ## Custom Network Interfaces [#custom-network-interfaces] You can change the network interface that Wings uses for all containers by editing the network name; it is by default set to `pterodactyl_nw`. For example, to enable Docker host mode change the network name to `host`. Changing network mode to `host` grants Pterodactyl direct access to all machine interfaces and Panel users can bind to any IP or Port even if it's not allocated to their container. You will lose all benefits of Docker network isolation. It is not recommended for public installations that are hosting other users' servers. ### Example of usage [#example-of-usage-1] ```yml docker: network: name: host network_mode: host ``` After making changes, the following commands will stop the Wings, remove the Pterodactyl network, and start the Wings again. Run at your own risk. `systemctl stop wings && docker network rm pterodactyl_nw && systemctl start wings` ## Enabling Cloudflare proxy [#enabling-cloudflare-proxy] Cloudflare proxying of the Wings isn't beneficial since users will be connecting to the machine directly and bypassing any Cloudflare protection. As such, your Node machine IP will still be exposed. To enable Cloudflare proxy, you must change the Wings port to one of the Cloudflare HTTPS ports with caching enabled (more info [here](https://developers.cloudflare.com/fundamentals/get-started/reference/network-ports/)), such as 8443, because Cloudflare only supports HTTP on port 8080. Select your Node in the Admin Panel, and on the settings tab, change the port. Make sure that you set "Not Behind Proxy" when using Full SSL settings in Cloudflare. Then on Cloudflare dashboard, your FQDN must have an orange cloud enabled beside it. You are unable to proxy the SFTP port through Cloudflare unless you have their enterprise plan. ## Container PID Limit [#container-pid-limit] You can change the total number of processes that can be active in a container at any given moment by changing the `container_pid_limit` value. The default value is `512`. You can set it to `0` to disable the limit completely. However, this is *not* recommended as the limit prevents malicious overloading of the node. Restart wings and your game server to apply the new limit. ### Example of usage [#example-of-usage-2] ```yml docker: ... container_pid_limit: 512 ... ``` ## Throttles Limits [#throttles-limits] You can use these settings to adjust or completely disable throttling. | Setting Key | Default Value | Notes | | :---------------------- | :-----------: | ----------------------------------------------------------------------------------------------------------------------------------- | | enabled | true | Whether or not the throttler is enabled | | lines | 2000 | Total lines that can be output in a given line\_reset\_interval period | | maximum\_trigger\_count | 5 | Amount of times throttle limit can be triggered before the server will be stopped | | line\_reset\_interval | 100 | The amount of time after which the number of lines processed is reset to 0 | | decay\_interval | 10000 | Time in milliseconds that must pass without triggering throttle limit before trigger count is decremented | | stop\_grace\_period | 15 | Time that a server is allowed to be stopping for before it is terminated forcefully if it triggers output throttle | | write\_limit | 0 | Impose I/O write limit for backups to the disk, 0 = unlimited. Value greater than 0 throttles write speed to the set value in MiB/s | | download\_limit | 0 | Impose a Network I/O read limit for archives, 0 = unlimited. Value greater than 0 throttles read speed to the set value in MiB/s | ### Example of usage [#example-of-usage-3] ```yml throttles: enabled: true lines: 2000 maximum_trigger_count: 5 line_reset_interval: 100 decay_interval: 10000 stop_grace_period: 15 ``` ## Installer Limits [#installer-limits] Defines the limits on the installer containers that prevents a server's installation process from unintentionally consuming more resources than expected. This is used in conjunction with the server's defined limits. Whichever value is higher will take precedence in the install containers. | Setting Key | Default Value | Notes | | :---------- | :-----------: | ----------------------------------------------------------------------------------------------------------- | | memory | 1024 | The maximum amount of memory install container can use unless server memory limit is higher than this value | | cpu | 100 | The maximum amount of cpu install container can use unless server cpu limit is higher than this value | ### Example of usage [#example-of-usage-4] ```yml installer_limits: memory: 1024 cpu: 100 ``` ## Other values [#other-values] More commonly discussed values. View all Wings config values and explanations in [these two files.](https://github.com/pterodactyl/wings/tree/develop/config) | Setting Key | Default Value | Notes | | ------------------------------ | :-----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | debug | false | Force Wings to run in debug mode | | tmpfs\_size | 100 | The size of the /tmp directory in MB when mounted into a container | | websocket\_log\_count | 150 | The number of lines to display in the console | | detect\_clean\_exit\_as\_crash | true | Mark server as crashed if it's stopped without user interaction, e.g., not pressing stop button | | (crash detection) timeout | 60 | Timeout between server crashes that will not cause the server to be automatically restarted | | app\_name | "Pterodactyl" | Changes the name of the daemon, shown in the panel's game console | | check\_permissions\_on\_boot | true | Check all file permissions on each boot. Disable this when you have a very large amount of files and the server startup is hanging on checking permissions | # Wings Wings is the server control plane from Pterodactyl. It has been rebuilt from the ground up using Go and lessons learned from our first Nodejs Daemon. ## Getting Started [#getting-started] If you're installing Wings for the first time, start with the [Installing Wings](/v1/wings/installing) guide. If you're migrating from the old Node.JS daemon (0.6.x), see the [Migrating to Wings](/v1/wings/migrating) guide. ## Documentation Sections [#documentation-sections] ### Wings 1.0 (Current) [#wings-10-current] * [Installing](/v1/wings/installing) - Complete installation guide for Wings * [Configuration](/v1/wings/configuration) - Advanced configuration options * [Upgrading](/v1/wings/upgrading) - Guide for upgrading Wings # Installing Wings Wings is the next generation server control plane from Pterodactyl. It has been rebuilt from the ground up using Go and lessons learned from our first Nodejs Daemon. You should only install Wings if you are running **Pterodactyl 1.x**. Do not install this software for previous versions of Pterodactyl. ## Supported Systems [#supported-systems] The following is a list of supported operating systems. Please be aware that this is not an exhaustive list, there is a high probability that you can run the software on other Linux distributions without much effort. You are responsible for determining which packages may be necessary on those systems. There is also a very high probability that new releases of the supported OSes below will work just fine, you are not restricted to only the versions listed below. | Operating System | Version | Supported | Notes | | ---------------------------------- | ------- | :-------: | --------------------------------------------------- | | **Ubuntu** | 22.04 | ✅ | | | | 24.04 | ✅ | | | **RHEL / Rocky Linux / AlmaLinux** | 8 | ✅ | | | | 9 | ✅ | | | **Debian** | 11 | ✅ | | | | 12 | ✅ | | | | 13 | ✅ | | | **Windows** | All | ❌ | This software will not run in Windows environments. | ## System Requirements [#system-requirements] To run Wings, you will need a Linux system capable of running Docker containers. Most VPS and almost all dedicated servers should be capable of running Docker, but there are edge cases. When your provider uses `Virtuozzo`, `OpenVZ` (or `OVZ`), or `LXC` virtualization, you will most likely be unable to run Wings. Some providers have made the necessary changes for nested virtualization to support Docker. Ask your provider's support team to make sure. KVM is guaranteed to work. The easiest way to check is to type `systemd-detect-virt`. If the result doesn't contain `OpenVZ` or`LXC`, it should be fine. The result of `none` will appear when running dedicated hardware without any virtualization. Should that not work for some reason, or you're still unsure, you can also run the command below. ```bash dane@pterodactyl:~$ sudo dmidecode -s system-manufacturer VMware, Inc. ``` ## Dependencies [#dependencies] * curl * Docker ### Installing Docker [#installing-docker] For a quick install of Docker CE, you can execute the command below: ```bash curl -sSL https://get.docker.com/ | CHANNEL=stable bash ``` If you would rather do a manual installation, please reference the [official Docker documentation](https://docs.docker.com/engine/install/) for how to install Docker CE on your server. Please be aware that some hosts install a modified kernel that does not support important docker features. Please check your kernel by running `uname -r`. If your kernel ends in `-xxxx-grs-ipv6-64` or `-xxxx-mod-std-ipv6-64` you're probably using a non-supported kernel. Ask your host to switch your server to your distribution's standard kernel. #### Start Docker on Boot [#start-docker-on-boot] If you are on an operating system with systemd (Ubuntu 16+, Debian 8+, CentOS 7+) run the command below to have Docker start when you boot your machine. ```bash sudo systemctl enable --now docker ``` #### Enabling Swap [#enabling-swap] Since the version 6.1 of the Linux kernel, swap is enabled by default. If you are running a kernel version 6.1 or newer, you can skip this step. To check your kernel version, run `uname -r`. On most systems, Docker will be unable to setup swap space by default. You can confirm this by running `docker info` and looking for the output of `WARNING: No swap limit support` near the bottom. Enabling swap is entirely optional, but we recommended doing it if you will be hosting for others and to prevent OOM errors. To enable swap, open `/etc/default/grub` as a root user and find the line starting with `GRUB_CMDLINE_LINUX_DEFAULT`. Make sure the line includes `swapaccount=1` somewhere inside the double-quotes. After that, run `sudo update-grub` followed by `sudo reboot` to restart the server and have swap enabled. Below is an example of what the line should look like, *do not copy this line verbatim. It often has additional OS-specific parameters.* ```text GRUB_CMDLINE_LINUX_DEFAULT="swapaccount=1" ``` Some Linux distros may ignore `GRUB_CMDLINE_LINUX_DEFAULT`. Therefore you might have to use `GRUB_CMDLINE_LINUX` instead should the default one not work for you. ## Installing Wings [#installing-wings] The first step for installing Wings is to ensure we have the required directory structure setup. To do so, run the commands below, which will create the base directory and download the wings executable. ```bash sudo mkdir -p /etc/pterodactyl curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")" sudo chmod u+x /usr/local/bin/wings ``` If you are using a server provided by OVH or SoYouStart please be aware that your main drive space is probably allocated to `/home`, and not `/` by default. Please consider using `/home/daemon-data` for server data. This can be easily set when creating the node. ## Configure [#configure] Once you have installed Wings and the required components, the next step is to create a node on your installed Panel. Go to your Panel administrative view, select Nodes from the sidebar, and on the right side click Create New button. After you have created a node, click on it and there will be a tab called Configuration. Copy the code block content, paste it into a new file called `config.yml` in `/etc/pterodactyl` and save it. Alternatively, you can click on the Generate Token button, copy the bash command and paste it into your terminal. When your Panel is using SSL, the Wings must also have one created for its FQDN. See the documentation for creating SSL certificates before continuing. ### Starting Wings [#starting-wings] To start Wings, simply run the command below, which will start it in debug mode. Once you confirmed that it is running without errors, use `CTRL+C` to terminate the process and daemonize it by following the instructions below. Depending on your server's internet connection pulling and starting Wings for the first time may take a few minutes. ```bash sudo wings --debug ``` ### Daemonizing (using systemd) [#daemonizing-using-systemd] Running Wings in the background is a simple task, just make sure that it runs without errors before doing this. Place the contents below in a file called `wings.service` in the `/etc/systemd/system` directory. ```text [Unit] Description=Pterodactyl Wings Daemon After=docker.service Requires=docker.service PartOf=docker.service [Service] User=root WorkingDirectory=/etc/pterodactyl LimitNOFILE=4096 PIDFile=/var/run/wings/daemon.pid ExecStart=/usr/local/bin/wings Restart=on-failure StartLimitInterval=180 StartLimitBurst=30 RestartSec=5s [Install] WantedBy=multi-user.target ``` Then, run the commands below to reload systemd and start Wings. ```bash sudo systemctl enable --now wings ``` ### Node Allocations [#node-allocations] Allocation is a combination of IP and Port that you can assign to a server. Each created server must have at least one allocation. The allocation would be the IP address of your network interface. In some cases, such as when behind NAT, it would be the internal IP. To create new allocations go to Nodes > your node > Allocation. Type `hostname -I | awk '{print $1}'` to find the IP to be used for the allocation. Alternatively, you can type `ip addr | grep "inet "` to see all your available interfaces and IP addresses. Do not use 127.0.0.1 for allocations. # Migrating to Wings This guide is for people looking to migrate from the old Node.JS daemon to Wings. Please see the [install guide](/v1/wings/installing) if you are trying to install Wings for the first time on a new node. You **must** be running Pterodactyl Panel 1.X in order to use Wings. You'll have a brief offline period as you perform this process, however no running game processes will be affected. Plus, chances are your Panel will be offline (or in maintenance mode) during this so your users should not notice anything out of the ordinary. ## Install Wings [#install-wings] The first step for installing the daemon is to make sure we have the required directory structure setup. To do so, run the commands below which will create the base directory and download the wings executable. ```bash mkdir -p /etc/pterodactyl curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")" chmod u+x /usr/local/bin/wings ``` ## Copy New Configuration File [#copy-new-configuration-file] Once you have installed Wings, you'll need to copy over a new configuration file from the Panel. This file is in a new format, and should be easier for you to manage and edit in the future. Simply copy and paste the code block and paste it into a file called `config.yml` within the `/etc/pterodactyl` directory and save it. Please note that any modifications you previously made to the configuration will be lost with this. If you have modifications to our default settings, the best option is to start Wings once with the copied configuration which will then populate all of the other configuration settings. From there you can make any adjustments as necessary. ## Remove Old Daemon [#remove-old-daemon] Now that Wings is installed, we need to remove all of the old daemon code from the server since it is not being used anymore. To do this, simply execute the following commands — assuming your old daemon is in the default `/srv/daemon` directory. ```bash # Stop the old daemon. systemctl stop wings # Delete the entire directory. There is nothing stored in here that we actually need for the # purposes of this migration. Remember, server data is stored in /srv/daemon-data. rm -rf /srv/daemon # Optionally, remove NodeJS from your system if it was not used for anything else. apt -y remove nodejs # or: yum remove nodejs ``` ### Remove Standalone SFTP [#remove-standalone-sftp] If you've used the standalone SFTP server with the old daemon, we need to remove it's systemd service as well, as it's no longer needed. You can do so using the following commands. ```bash # stop and disable the standalone sftp systemctl disable --now pterosftp # delete the systemd service rm /etc/systemd/system/pterosftp.service ``` ## Daemonize Wings [#daemonize-wings] You'll then need to edit your existing `systemd` service file for Wings to point to the new control software. To do this, open `/etc/systemd/system/wings.service` and replace the entire contents of the file with the following: ```text [Unit] Description=Pterodactyl Wings Daemon After=docker.service [Service] User=root WorkingDirectory=/etc/pterodactyl LimitNOFILE=4096 PIDFile=/var/run/wings/daemon.pid ExecStart=/usr/local/bin/wings Restart=on-failure StartLimitInterval=600 [Install] WantedBy=multi-user.target ``` Then, start wings. ```bash systemctl daemon-reload systemctl enable --now wings ``` If you encounter issues starting Wings at this point, run the following command to start Wings directly and check for any specific error output. ```bash sudo wings --debug ``` # Upgrading Wings Upgrading Wings is a painless process and should take less than a minute to complete. ## Wings Version Requirements [#wings-version-requirements] Each version of Pterodactyl Panel also has a corresponding minimum version of Wings that is required for it to run. Please see the chart below for how these versions line up. In most cases your base Wings version should match that of your Panel. | Panel Version | Wings Version | Supported | | ------------- | ------------- | --------- | | 1.0.x | 1.0.x | | | 1.1.x | 1.1.x | | | 1.2.x | 1.2.x | | | 1.3.x | 1.3.x | | | 1.4.x | 1.4.x | | | 1.5.x | 1.4.x | | | 1.6.x | 1.4.x | | | 1.7.x | 1.5.x | | | 1.8.x | 1.6.x | | | 1.9.x | 1.6.x | | | 1.10.x | 1.7.x | | | **1.11.x** | **1.11.x** | ✅ | *NOTE: There are no 1.8.x, 1.9.x, or 1.10.x releases of Wings.* ## Download Updated Binary [#download-updated-binary] First, download the updated wings binary into `/usr/local/bin`. You will need to stop Wings briefly. *Your running servers **will not** be affected.* ```bash systemctl stop wings curl -L -o /usr/local/bin/wings "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$([[ "$(uname -m)" == "x86_64" ]] && echo "amd64" || echo "arm64")" chmod u+x /usr/local/bin/wings ``` ## Restart Process [#restart-process] Finally, restart the wings process. Your running servers will not be affected and any open connections to the instance will re-connect automatically. ```bash systemctl restart wings ``` # API Overview ## Introduction [#introduction] The Panel has three APIs. Choose the one that matches what you want to do: | API | Path | Use it to | | ----------- | ------------------ | ----------------------------------------------------------------------- | | Client | `/api/client` | Manage your account and the servers you have access to. | | Application | `/api/application` | Manage users, nodes, and servers, for example from a billing system. | | Admin | `/api/admin` | Manage the whole Panel, including eggs, tags, settings, and extensions. | The Panel's own login pages use the browser authentication endpoints under `/auth`. The paths above are the same in every version. The `v2` in this documentation's address is not part of the API path. In the examples, replace `https://panel.example.com` with the address of your Panel. ## Authentication [#authentication] Send your API key in the `Authorization` header of every request: ```http Authorization: Bearer YOUR_API_KEY ``` Each API accepts different keys: | API | Accepted keys | | ----------- | ----------------------------------------------------------------- | | Client | A Client API key. It can only do what its user can do. | | Application | An Application API key, or a root administrator's Client API key. | | Admin | A root administrator's Client API key. | Application API keys only work with the Application API, where each key is limited to the permissions it was created with. The Client API and the Admin API reject them. API keys created on 1.x keep working after you upgrade. ### Browser Sessions [#browser-sessions] The Panel's frontend signs in with a session cookie instead of an API key. First request `/sanctum/csrf-cookie`, then keep the cookies it returns, and send the CSRF token with every request that changes data. The examples use a session cookie named `pterodactyl_session`. Your Panel may use a different name if you have set `APP_NAME` or `SESSION_COOKIE`. ## Where to Start [#where-to-start] * [List your servers](./client-api/clientListClientServers.mdx) with the Client API. * [List users](./application-api/applicationListUsers.mdx) with the Application API. * [List users](./admin-api/adminListUsers.mdx) with the Admin API. * [Sign in](./authentication/authLogin.mdx) with a browser session. If you are moving an integration from 1.x, read [Changes From 1.x](../upgrading/changes-from-v1.mdx) first. The nest endpoints have been removed. # Backend An extension's backend runs inside the Panel like a Laravel package. This page continues the Server Notes example from [Building Extensions](./building.mdx). ## The Service Provider [#the-service-provider] The backend starts in a service provider. It works like a Laravel service provider and must extend `Pterodactyl\Extensions\ExtensionProvider`: ```php registerApiRoutes(); $this->loadExtensionMigrations(); } } ``` Use `register` for container bindings and `boot` for everything else. The base class provides helpers for routes, migrations, settings, views, and translations. The Panel loads your provider on every request. If it throws an exception, the Panel skips your extension, shows the error in the admin area, and keeps running. ## Routes [#routes] Put your routes in the `routes` directory. `registerApiRoutes` loads each of these files that exists, with its own URL prefix and middleware: | File | URL prefix | Who can call it | | ------------------------ | ---------------------------------------------- | ---------------------------------------------------- | | `routes/client.php` | `/api/client/extensions/{id}` | Any signed-in user, or a Client API key. | | `routes/server.php` | `/api/client/servers/{server}/extensions/{id}` | Users with access to the server. | | `routes/admin.php` | `/api/admin/extensions/{id}` | Root administrators. | | `routes/application.php` | `/api/application/extensions/{id}` | Root administrators, including Application API keys. | These routes use the same authentication, two-factor, and rate-limit middleware as the Panel's own API, so your extension does not handle sign-in. Server Notes adds two server routes: ```php where('server_id', $server->id)->first(); return new JsonResponse([ 'body' => $note->body ?? '', 'updated_at' => $note?->updated_at?->toAtomString(), ]); } ``` For a web route instead of an API route, call `registerWebRoutes` or `registerAuthenticatedWebRoutes` with the path to a route file. Web routes are served under `/extensions/{id}`. See the [Extension Reference](./reference.mdx#route-helpers). ## Authorization [#authorization] The Panel checks that the user may access the server before your server routes run. Users without access receive a `404` response. To check a specific permission, call `can` with one of the Panel's [subuser permissions](./reference.mdx#permissions). The server owner and root administrators pass every check. Server Notes lets any user read the notes, but only users who may edit files can change them: ```php public function update(Request $request, Server $server): JsonResponse { abort_unless($request->user()->can('file.update', $server), 403, 'You do not have permission to edit notes.'); // ... } ``` To define permissions for your own features, call `registerPermissions` in `boot`: ```php $this->registerPermissions('Manage server notes.', [ 'read' => 'Read server notes.', 'update' => 'Change server notes.', ]); ``` These keys become `ext.server-notes.read` and `ext.server-notes.update` and appear in the server's subuser permission editor. Use them with `$user->can(...)`, `useServerPermission(...)`, and a server screen's `permission` list. A wildcard such as `ext.server-notes.*` covers only that extension's permissions. ## Database Migrations [#database-migrations] Put your migrations in `database/migrations` and call `loadExtensionMigrations` in `boot`. The Panel runs them when an administrator enables your extension, before marking it enabled. Start table names with `ext_` and your extension's ID, using underscores instead of hyphens, to keep them apart from the Panel's own tables: ```php Schema::create('ext_server_notes_notes', function (Blueprint $table) { $table->id(); $table->unsignedInteger('server_id')->unique(); $table->text('body'); $table->timestamps(); $table->foreign('server_id')->references('id')->on('servers')->cascadeOnDelete(); }); ``` Disabling or removing the extension keeps its tables, so reinstalling it brings its data back. Package recovery does not undo schema changes, so design migrations so you can identify and recover from a partially completed change. Use Eloquent models for your tables as usual: ```php registerSettings(new ExtensionSettingsDefinition($this->settings(), [ ExtensionSettingDefinition::make('max_length', 'max_length', 2000, ['required', 'integer', 'min:100', 'max:20000']) ->label('Maximum note length') ->help('The longest note, in characters, that users can save.') ->field('number') ->frontend() ->frontendType('number'), ])); } ``` The `make` method takes the setting's storage key, form field name, default value, and validation rules. Give each setting rules that match the values you accept. The `field` method chooses the form control: `text`, `password`, `number`, `toggle`, or `select`. For `select`, pass the options as the second argument: ```php ExtensionSettingDefinition::make('mode', 'mode', 'simple', ['required', 'in:simple,advanced']) ->field('select', [ ['value' => 'simple', 'label' => 'Simple'], ['value' => 'advanced', 'label' => 'Advanced'], ]); ``` To read a setting in backend code, type-hint `ExtensionSettingsRegistry $registry` in your controller or job and get your definition from it. Unsaved settings return their default value: ```php use Pterodactyl\Services\Extensions\ExtensionSettingsRegistry; $maxLength = $registry->get('server-notes')->get('max_length'); ``` Server Notes uses the setting to validate new notes: ```php public function update(Request $request, Server $server, ExtensionSettingsRegistry $settings): JsonResponse { abort_unless($request->user()->can('file.update', $server), 403, 'You do not have permission to edit notes.'); $data = $request->validate([ 'body' => ['present', 'nullable', 'string', 'max:'.$settings->get('server-notes')->get('max_length')], ]); $note = Note::query()->updateOrCreate(['server_id' => $server->id], ['body' => $data['body'] ?? '']); return new JsonResponse([ 'body' => $note->body, 'updated_at' => $note->updated_at->toAtomString(), ]); } ``` The `frontend` method sends the setting's value to your frontend bundle as `config`. **Every signed-in user can read frontend settings**, so never mark a secret as a frontend setting. Pages shown before sign-in, such as the login page, still load your bundle, but with an empty `config`. `frontendType` declares the public value's type, such as `number`, `boolean`, or `string`, and the Panel checks it before exposing the value. For values your extension manages itself and administrators should not edit, use the key-value store returned by `$this->settings()`. It has `get`, `set`, `forget`, and `all` methods. ## Scoped Settings and Secrets [#scoped-settings-and-secrets] The store returned by `settings()` is global to your extension. Use `forUser($user)` or `forServer($server)` to keep separate values per persisted user or server: ```php $preferences = $this->settings()->forUser($user); $preferences->set('show_preview', true); $serverSettings = $this->settings()->forServer($server); $serverSettings->set('summary_enabled', true); $serverSettings->setSecret('webhook_token', $token); ``` Read a secret with `get`. Use `setSecret` every time you replace it so it stays encrypted. The settings definition also supports `forUser` and `forServer` when scoped values need defaults, normalization, and validation. For a secret in the administrator's settings form, add `secret()` to its definition: ```php ExtensionSettingDefinition::make('webhook_token', 'webhook_token', '', ['nullable', 'string', 'max:2000']) ->label('Webhook token') ->secret(); ``` A secret uses a password control, is encrypted in the database, and is returned blank to the admin form. An empty update keeps the saved secret. Secrets cannot use `frontend()`. `field('password')` on its own does not enable encryption. ## Server Operation Events [#server-operation-events] To react to a completed server operation, register a listener with `listenToServerOperations` in your provider. It receives a `Pterodactyl\Events\Server\OperationCompleted` object with the server UUID, operation, success flag, and resource UUID. The operations are `provision`, `install`, `reinstall`, and `backup`. Events are dispatched after the database transaction commits; provisioning is reported after Wings accepts the server. The values are read-only, so a listener does not depend on a model being available later. See [Server Operation Events](./reference.mdx#server-operation-events) for the event fields. If a listener throws, the Panel records the failure against the extension. ## Job Progress [#job-progress] For long-running work, inject `Pterodactyl\Services\Extensions\ExtensionJobProgress` into your controller or job. Create a snapshot for the user who starts the work: ```php $job = $progress->begin('server-notes', $user, $server, 'ext.server-notes.update'); ``` Return `$job->id` to the frontend and pass it to your worker, which updates it as the work proceeds: ```php $progress->update('server-notes', $jobId, 40, 'Reading server notes.'); $progress->update('server-notes', $jobId, 100, 'Summary ready.', 'completed'); ``` Progress only moves forward, from 0 to 100. The status is `running`, `completed`, or `failed`; a finished snapshot cannot be changed. Omit the server and permission arguments for a user-only job. User jobs, and server jobs without a permission, are visible only to their creator and root administrators. A server job with a permission is visible to users who still have that permission and access to the server. Web requests and workers must use the same cache store. Snapshots expire according to `extensions.progress_retention_seconds`, which defaults to one hour after the last update. Store results or audit history separately. To show the snapshot in a screen or slot, use `useExtensionJobProgress`. Pass the server UUID for a server job: ```tsx import { Spinner, useExtensionJobProgress } from '@pterodactyl/sdk'; export function JobProgress({ id, serverUuid }: { id: string; serverUuid: string }) { const progress = useExtensionJobProgress(id, { serverUuid }); if (progress.error) return

Progress is unavailable.

; if (!progress.data) return ; return

{progress.data.message} ({progress.data.percent}%)

; } ``` The hook polls while the job runs, reads the latest snapshot after reconnecting, and stops on completion, failure, or loss of access. ## Views and Translations [#views-and-translations] For Blade views, put them in `resources/views` and call `loadExtensionViews`. For translations, put them in `resources/lang` and call `loadExtensionTranslations`. Both use the `ext-{id}` namespace, such as `ext-server-notes::notes.title`. # Building Extensions ## Introduction [#introduction] An extension has a PHP backend that runs inside the Panel like a Laravel package, a React frontend that the Panel loads in the browser, or both. This guide builds an example extension, **Server Notes**, that adds a **Notes** page to every server, shows the notes above the console, stores them in its own database table, and has a setting for the maximum note length. Read [Extensions](./index.mdx) first to learn how extensions are installed and managed. The guide has five parts: 1. **Building Extensions** (this page): create the extension and write its manifest. 2. [Backend](./backend.mdx): the service provider, routes, database, settings, and background work. 3. [Frontend](./frontend.mdx): slots, screens, tabs, table columns, and component replacements. 4. [Data and Styling](./data-and-styling.mdx): Panel data, your API, components, and translations. 5. [Testing and Packaging](./testing-and-packaging.mdx): tests, builds, packages, and security. ## Requirements [#requirements] You need: * A Pterodactyl 2.0 source checkout, set up as a development Panel. * PHP 8.3 or newer and Composer. * Node.js 22.12 or newer. Extensions created from a Panel checkout use its local `@pterodactyl/sdk` package. After installing the Panel's frontend dependencies, build the SDK test runtime from the Panel root: ```bash npm run sdk:testing ``` Extension tests use this runtime. Rebuild it when you change the SDK source. ## Creating an Extension [#creating-an-extension] From your Panel checkout, run `p:extension:make`: ```bash php artisan p:extension:make server-notes --name="Server Notes" --description="Keep shared notes on each server." ``` This creates the extension in the Panel's `extensions` directory. Install its frontend dependencies and build it: ```bash cd extensions/server-notes npm install npm run typecheck npm test npm run build cd ../.. ``` Then install and enable it in your development Panel: ```bash php artisan p:extension:install extensions/server-notes --enable ``` `p:extension:make` accepts these options: | Option | Description | | --------------- | ------------------------------------------------------------------------------------------------------------ | | `--name` | Display name. Defaults to the ID in title case. | | `--description` | Short description. | | `--author` | Author's name. | | `--prefix` | Tailwind prefix for the extension's classes, 2 to 12 lowercase letters. Defaults to one derived from the ID. | | `--no-ui` | Create a backend-only extension, with no frontend. | | `--out` | Create the extension in a different directory. | | `--force` | Replace an existing directory. | ### Extension IDs [#extension-ids] The ID names your extension everywhere: its directory, URLs, and settings. It must start with a lowercase letter, contain only lowercase letters, numbers, and hyphens, and be at most 48 characters. `pterodactyl`, `panel`, and `core` are reserved. The extension's directory must have the same name as its ID. ### Directory Structure [#directory-structure] With the files from this guide added, Server Notes looks like this: ```text server-notes/ ├── .github/workflows/ci.yml ├── .gitignore ├── extension.json ├── README.md ├── database/ │ └── migrations/ │ └── 2026_09_29_000000_create_ext_server_notes_notes_table.php ├── routes/ │ └── server.php ├── scripts/ │ └── externalize-api.mjs ├── src/ │ ├── ServerNotesProvider.php │ ├── Http/Controllers/NoteController.php │ ├── Models/Note.php │ └── client/ │ ├── index.tsx │ ├── styles.css │ ├── api.ts │ ├── config.ts │ ├── NotePreview.tsx │ └── screens/ │ ├── NotesScreen.tsx │ └── NotesScreen.spec.tsx ├── openapi-ts.config.ts ├── package.json ├── postcss.config.mjs ├── tsconfig.json ├── vite.config.mjs └── vitest.config.mjs ``` The command also creates sample client routes in `routes/client.php`, a `StatusController`, and a `ServerScreen` with a test. Server Notes replaces them with the files above. `openapi-ts.config.ts` and `scripts/externalize-api.mjs` generate API clients, and the GitHub Actions workflow runs type checks, tests, and builds. With `--no-ui`, the command creates only the manifest, provider, sample controller and routes, migrations directory, `.gitignore`, and `README.md`. ## The Manifest [#the-manifest] The `extension.json` file in the extension's root tells the Panel what the extension is and how to load it: ```json { "$schema": "./node_modules/@pterodactyl/sdk/manifest.schema.json", "id": "server-notes", "name": "Server Notes", "version": "1.0.0", "requires": { "panel": "^2.0.0-dev", "sdk": "^2.0.0-beta.4", "php": "^8.3" }, "description": "Keep shared notes on each server.", "provider": "ServerNotes\\ServerNotesProvider", "autoload": { "ServerNotes\\": "src" }, "ui": { "entry": "dist/client.js", "mode": "native", "prefix": "sn", "screens": [ { "id": "notes", "area": "server", "path": "notes", "nav": { "label": "Notes" } } ] } } ``` The `provider` and `autoload` keys load your PHP code. The `ui` key loads your frontend bundle and declares its pages. `ui.prefix` is the Tailwind prefix for your extension's classes, derived from the ID unless you pass `--prefix`. The Panel rejects a build whose Tailwind utilities or theme variables do not use it. Leave out the keys for any half you do not use. See the [Extension Reference](./reference.mdx#manifest) for every key. ### Icon [#icon] Administrators see each extension as a card on the **Extensions** page. Without an `icon`, the card shows the extension's initials. Set `icon` to a [Lucide](https://lucide.dev/icons) icon name: ```json "icon": "notebook-pen" ``` Or ship an image in the extension and give its path: ```json "icon": "icon.png" ``` The image must be a PNG, JPEG, or WebP file of at most 512 KB and 2048 pixels on each side. Use a square image of at least 128 pixels. `p:extension:pack` includes it in the package wherever it is, and `p:extension:doctor` warns if the Panel cannot show it. See [Extension Icons](./reference.mdx#extension-icons) for the full rules. ### Compatibility [#compatibility] Declare the versions your extension supports with `requires.panel`, `requires.sdk`, and `requires.php`. Constraints use Composer syntax, such as `^2.0.0-beta.4` or `>=8.3 <9.0`. The values above match the development Panel and its SDK. To depend on another extension, declare its ID and version constraint under `requires.extensions`: ```json "requires": { "extensions": { "shared-notes": "^1.0" } } ``` The required extension must be enabled before yours can be. The Panel loads dependencies first, rejects dependency cycles, and skips extensions with unmet requirements. If a dependency fails, its dependents do not load either. The `$schema` entry enables manifest completion in editors that support JSON Schema. The Panel validates the manifest when the package is installed and enabled. # Data and Styling These tools work in any frontend component: slots, screens, columns, and replacements. ## Using Panel Data [#using-panel-data] The SDK has hooks for the data your pages need. Server Notes uses two: ```tsx import { useCurrentServerRequired, useServerPermission } from '@pterodactyl/sdk'; const server = useCurrentServerRequired(); const canEdit = useServerPermission('file.update'); ``` `useCurrentServerRequired` returns the current server on server pages. `useServerPermission` returns whether the current user has a permission on that server. Other hooks return the current user, the site settings, and live server events. See the [SDK reference](./reference.mdx#hooks). The SDK also exposes the Panel's queries and mutations for files, startup configuration, and backups: ```tsx import { useCurrentServerRequired, useServerFiles } from '@pterodactyl/sdk'; const server = useCurrentServerRequired(); const files = useServerFiles(server.attributes.uuid, '/', (response) => response.data); ``` Pass the server UUID to these hooks so they share the native page's cache entries. Query option factories are available for `useQuery`, prefetching, or direct cache access. File writes, startup changes, and backup creation have mutation hooks that update or invalidate the same cache. Startup writes are serialized per server. If your own endpoint changes native data, await `invalidateServerData(uuid, domains)` after it succeeds. Supported domains are `server`, `files`, `fileContent`, `startup`, and `backups`. See [Server Data](./reference.mdx#server-data) for queries and [Mutations](./reference.mdx#mutations) for writes. ## Calling Your API [#calling-your-api] Call your backend with the SDK's `http` client. It is the client the Panel uses, so requests are already signed in. Server Notes keeps its API calls in `src/client/api.ts`: ```ts import { http } from '@pterodactyl/sdk'; export interface Note { body: string; updated_at: string | null; } const noteUrl = (server: string) => `/api/client/servers/${server}/extensions/server-notes/note`; export const noteQueryKey = (server: string) => ['server-notes', server] as const; export async function getNote(server: string): Promise { const { data } = await http.get(noteUrl(server)); return data; } export async function saveNote(server: string, body: string): Promise { const { data } = await http.put(noteUrl(server), { body }); return data; } ``` The Panel shares its copy of TanStack Query with extensions, so you can use `useQuery` and `useMutation` directly. The Notes screen loads and saves the note like this: ```tsx import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Button, ServerContentBlock, Spinner, TextArea, TitledGreyBox, httpErrorToHuman, toast, useCurrentServerRequired, useServerPermission, } from '@pterodactyl/sdk'; import { getNote, noteQueryKey, saveNote } from '../api'; import { maxLength } from '../config'; export default function NotesScreen() { const server = useCurrentServerRequired(); const identifier = server.attributes.identifier; const canEdit = useServerPermission('file.update'); const queryClient = useQueryClient(); const [body, setBody] = useState(''); const note = useQuery({ queryKey: noteQueryKey(identifier), queryFn: () => getNote(identifier) }); useEffect(() => { if (note.data) { setBody(note.data.body); } }, [note.data]); const save = useMutation({ mutationFn: () => saveNote(identifier, body), onSuccess: (saved) => { queryClient.setQueryData(noteQueryKey(identifier), saved); toast.success('Notes saved.'); }, onError: (error) => toast.error(httpErrorToHuman(error)), }); return ( {note.isLoading ? ( ) : (