Install the Pterodactyl Panel for game servers

Published on 17 min read

Panel and Wings are two separate programs with two separate jobs. Once you have understood that, Pterodactyl installs in half an hour. If you have not, you spend days hunting for the error.

Pterodactyl is the most widely used free panel for game servers. Its reputation for being complicated almost never comes from the installation itself, but from a single misunderstanding: Pterodactyl is not one program, it is two. This guide separates the two cleanly, shows the differences between Debian and Ubuntu, and then works through the error messages that send people to a search engine with the exact wording.

Panel and Wings: two programs, two roles

The Panel is a PHP application built on Laravel. It serves the web interface, manages users, permissions, databases and schedules, and stores everything in its own MySQL or MariaDB database. The Panel never starts a game server itself. It does not even know about Docker.

Wings is a single program written in Go. It runs on every machine that is actually meant to run game servers, talks to the Docker daemon, starts containers, streams the console and provides SFTP access. Wings has no web interface and no database. It listens on an HTTP port and waits for instructions from the Panel.

The two talk to each other exclusively over HTTP, in both directions, using signed tokens. Three consequences follow, and you should accept them from the start:

  • The Panel has to reach the node through a domain name, not through an IP. The certificate is tied to the name.
  • Panel and Wings have to speak the same protocol. An HTTPS panel plus HTTP Wings does not work, the browser blocks the console connection.
  • Both clocks have to be correct. The tokens are only valid for a few minutes.

Once those three sentences have sunk in, you have already avoided half of the typical Pterodactyl problems.

Requirements and choosing the system

Pterodactyl 1.11 and newer requires PHP 8.2 or 8.3. This is the point where most guides get vague, because the distributions ship very different versions. As of July 2026, the situation in the default repositories looks like this:

SystemPHPDatabasenginx
Debian 128.2 (works)MariaDB 10.111.22
Debian 138.4 (too new)MariaDB 11.81.26
Ubuntu 24.048.3 (works)MySQL 8.0 or MariaDB 10.111.24
Ubuntu 22.048.1 (too old)MySQL 8.0 or MariaDB 10.61.18
Debian 117.4 (too old)MariaDB 10.51.18

This guide is pure Debian and Ubuntu material. On AlmaLinux, Rocky Linux and Oracle Linux there is no apt, so none of the package commands below apply there.

Practical consequence: Debian 12 and Ubuntu 24.04 are the two systems on which the Panel runs without third-party repositories. On Ubuntu 22.04 you need the PPA from Ondřej Surý, on Debian 13 its Debian counterpart, because PHP 8.4 provides the meta package there and the Panel's composer.json explicitly asks for ^8.2 || ^8.3. In practice the Composer run does complete under PHP 8.4, but that is unconfirmed territory and not a state you want to run a production server in. If you want to save yourself the extra repository, take Debian 12 or Ubuntu 24.04.

The two systems that are too old do not fail on the package command, by the way, they fail two steps later, and that is what makes them so treacherous. apt install php completes with exit code 0 on Ubuntu 22.04 (PHP 8.1.2) and Debian 11 (PHP 7.4.33), and only the Composer run then aborts, on Ubuntu 22.04 with brick/math requires php (^8.2) failed, on Debian 11 with aws/aws-sdk-php requires php (>=8.1) failed. If you have to stay on one of those systems, install explicitly versioned packages from the third-party repository (php8.3, php8.3-cli, php8.3-fpm and so on) instead of the php meta package, otherwise the distribution version takes over again.

A second difference that regularly surprises people: Debian ships no mysql-server package at all. MariaDB is the default there, and that is perfectly fine, since Pterodactyl requires MariaDB 10.2 or newer. Anyone typing apt install mysql-server on Debian gets E: Unable to locate package mysql-server and then goes looking in the wrong place.

These PHP rules do not apply to the node running Wings at all, by the way. Wings is a statically linked Go program and only needs Docker and a reasonably recent kernel version. The node can happily run Debian 13 while the Panel runs on Debian 12.

Installing the Panel

All of the following commands run as root. The base packages first. Make sure you take the PHP extensions along in full, because a missing php-bcmath only shows up during the Composer run.

apt update
apt -y install curl ca-certificates gnupg lsb-release tar unzip git
apt -y install mariadb-server nginx redis-server
apt -y install php php-cli php-common php-gd php-mysql php-mbstring php-bcmath php-xml php-fpm php-curl php-zip

Check right away whether the version fits before you carry on:

php -v
php -m | grep -E "bcmath|mbstring|curl|zip|gd|xml"

Then Composer and the Panel files:

curl -sS https://getcomposer.org/installer -o /tmp/composer-setup.php
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
mkdir -p /var/www/pterodactyl
curl -Lo /var/www/pterodactyl/panel.tar.gz https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz
tar -xzf /var/www/pterodactyl/panel.tar.gz -C /var/www/pterodactyl

The database. Create the user on 127.0.0.1 and not on localhost, otherwise socket access kicks in and Laravel later gets an access denied even though the password is correct. How to tell the two cases apart is described in our post on Access denied for user.

mariadb -u root -e "CREATE DATABASE panel;"
mariadb -u root -e "CREATE USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY 'HierEinLangesPasswort';"
mariadb -u root -e "GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'127.0.0.1' WITH GRANT OPTION;"
mariadb -u root -e "FLUSH PRIVILEGES;"

The database should be secured afterwards, and we have a separate post about securing MariaDB and MySQL.

Now the actual setup. The p:environment commands are interactive and ask for the panel URL, time zone, cache driver and database credentials:

cd /var/www/pterodactyl
cp .env.example .env
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
php artisan key:generate --force
php artisan p:environment:setup
php artisan p:environment:database
php artisan migrate --seed --force
php artisan p:user:make
chown -R www-data:www-data /var/www/pterodactyl/*

In p:environment:setup, pick Redis as the session and cache driver, then the queue described further below works properly. And enter the panel URL with https://. An http:// at this point produces mixed content later on, plus a console that waits forever for a connection.

nginx and the version trap

The basics of setting up the web server are covered in our post on installing nginx. For Pterodactyl there are two details that matter, and ready-made templates from the internet regularly fail on both.

First the PHP-FPM socket. The file name contains the PHP version and differs from system to system. Look it up instead of guessing:

systemctl status php8.2-fpm
ls /run/php/

On Debian 12 the socket is called php8.2-fpm.sock, on Ubuntu 24.04 php8.3-fpm.sock. A wrong path here produces exactly the 502 Bad Gateway page that so many people search for. The status query comes first on purpose: the socket file is only created once the FPM service is running. If it has not been started, /run/php/ is empty and you wrongly conclude that the path is wrong. If the service is not running yet, systemctl enable --now php8.2-fpm with the version number matching your installation helps.

Second the HTTP/2 syntax. The new http2 on; directive only exists from nginx 1.25.1 onwards. On Debian 12 (1.22), Ubuntu 22.04 (1.18) and on Ubuntu 24.04 (1.24) as well, you have to use the old form listen 443 ssl http2;, otherwise the start aborts with nginx: [emerg] unknown directive "http2". Only Debian 13 with nginx 1.26 understands both forms.

server {
    listen 443 ssl http2;
    server_name panel.example.com;
    root /var/www/pterodactyl/public;
    index index.php;
    client_max_body_size 100m;

    ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param HTTP_PROXY "";
    }
}

Queue and schedule

Without these two building blocks the Panel looks functional, but it sends no mail and runs no scheduled tasks. The structure of the unit file is explained in detail in our post on creating a systemd service, and here is the finished version:

[Unit]
Description=Pterodactyl Queue Worker
After=redis-server.service

[Service]
User=www-data
Group=www-data
Restart=always
RestartSec=5s
ExecStart=/usr/bin/php /var/www/pterodactyl/artisan queue:work --queue=high,standard,low --sleep=3 --tries=3

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now redis-server
systemctl enable --now pteroq.service

On top of that comes one entry in root's crontab, and you can find details on the syntax in the post setting up a cron job on Linux:

* * * * * php /var/www/pterodactyl/artisan schedule:run >> /dev/null 2>&1

Installing Wings on the node

From here on you work on the machine that is going to run the game servers. That can be the same server, but it does not have to be. Docker is the prerequisite, and we described how to set it up in the post installing Docker on Debian and Ubuntu.

mkdir -p /etc/pterodactyl
curl -L -o /usr/local/bin/wings https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_amd64
chmod u+x /usr/local/bin/wings
wings version

On ARM servers the file name is wings_linux_arm64. If you pull the wrong package, the shell simply reports cannot execute binary file: Exec format error. Do not be puzzled by the output of the last line: wings version answers with a double v, so something like wings vv1.13.1. That is how upstream ships it and it is not a sign of a broken installation.

You do not write the file /etc/pterodactyl/config.yml yourself. It gets created automatically in a moment. For now, only create the service:

[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

Do not start it yet. Without a configuration, Wings aborts immediately with a message that essentially points to a missing configuration file at /etc/pterodactyl/config.yml. At this stage that is completely normal and not an error.

A word on swap: many older guides ask for swapaccount=1 in /etc/default/grub. That affects systems with cgroup v1 only. Debian 12 and 13 as well as Ubuntu 22.04 and 24.04 use cgroup v2 by default, where the entry is unnecessary. You can check with docker info. If WARNING: No swap limit support appears there, the memory limit for swap does not take effect. How to size swap sensibly in the first place is covered in the post setting up swap.

A certificate for the node, not just for the Panel

The most common mistake in reasoning: you get a certificate for panel.example.com and then wonder why the node does not work. Wings needs its own certificate for its own domain name, for example node1.example.com. Both names may point to the same IP, but they are two names.

A pure node runs no web server, so the standalone mode of Certbot is the simplest route. Port 80 has to be reachable from outside for a short while:

apt -y install certbot
certbot certonly --standalone -d node1.example.com

If you run many nodes, a wildcard certificate is the better option, and we describe that in the post Let's Encrypt wildcard certificate.

Two stumbling blocks that cost a lot of time:

  • Wings reads the certificate files at startup. After a renewal the service has to be reloaded. Put a small script containing systemctl restart wings into /etc/letsencrypt/renewal-hooks/deploy/. Without it the node runs flawlessly for 90 days and then fails for no apparent reason.
  • Turn off the Cloudflare proxy for the node name. The orange cloud terminates the TLS connection and replaces the certificate. The Panel then receives a certificate that does not match the expected issuer, and the console connection over WebSocket behaves unpredictably. The A record for the node belongs on grey.

Creating and connecting the node

In the Panel, first create a location under Admin, Locations, then the node under Nodes. The fields that actually count:

  • FQDN: node1.example.com, exactly the name from the certificate.
  • Communicate over SSL: enabled if the Panel runs over HTTPS. Otherwise do not even start.
  • Behind Proxy: only enable this if there really is a reverse proxy in front of Wings and it terminates the TLS.
  • Daemon Port: 8080. Daemon SFTP Port: 2022.
  • Memory and Disk: the limits the Panel respects when distributing servers.

After saving, open the Configuration tab of the node. There the Panel generates a command with a one-time token. Run it on the node:

cd /etc/pterodactyl
wings configure --panel-url https://panel.example.com --token TOKEN --node 1

Wings uses it to fetch its complete configuration and writes /etc/pterodactyl/config.yml. Check the contents: if the panel address there reads http:// instead of https://, you copied the command from an environment where the panel URL is stored incorrectly. Fix it at the root with php artisan p:environment:setup, not by hand in the YAML file.

After that, enter the IP of the node and the port ranges you want in the Allocations tab, for example 25565 to 25600 for Minecraft. Without at least one free allocation you cannot create a server.

Only now start it:

systemctl enable --now wings
systemctl status wings

The ports have to be open in the firewall. With ufw, whose basics we cover in the post setting up the ufw firewall:

ufw allow 8080/tcp
ufw allow 2022/tcp
ufw allow 25565:25600/tcp
ufw allow 25565:25600/udp

When Wings does not connect

The Panel shows a red icon on the node, or it throws an error when you create a server. Work through the messages one by one, they are surprisingly clear.

cURL error 7: Failed to connect ... Connection refused

The panel server cannot reach the port. Either Wings is not running, or the firewall is blocking, or the service is listening on the wrong address. Check in this order:

systemctl status wings
ss -tlnp | grep 8080
journalctl -u wings -n 50 --no-pager

And from the panel server, which is the decisive test:

curl -v https://node1.example.com:8080

An HTTP response, even a 404 with JSON content, counts as a success at this point. It proves that DNS, firewall, port and TLS work together.

cURL error 60: SSL certificate problem

The certificate of the node is not accepted. With self signed certificate you are using a self-created certificate, which the Panel cannot deal with, because the underlying library has no way to make an exception. With unable to get local issuer certificate the intermediate chain is usually missing, which means your Wings configuration points to cert.pem instead of fullchain.pem. With certificate has expired the renewal has run, but Wings still has the old file in memory, see the restart hook further above.

cURL error 28: Operation timed out

No answer, no reset. That smells of a firewall dropping packets instead of rejecting them, or of a node behind NAT. One special case: Panel and Wings on the same server, with the Panel addressing its own public IP. Some networks do not loop that back. The remedy is an entry in /etc/hosts on the panel server that points the node name at the internal address.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock

Wings is running, Docker is not. systemctl status docker settles that in one line. The node often still shows up as reachable in the Panel because the status query works, but every server start fails.

The node answers, but every action is rejected

A token does not match. This happens after another wings configure with an old token, or after the node was deleted and recreated in the Panel. Solution: generate a fresh token in the Configuration tab in the Panel, run the command again, restart Wings. Do not guess around in the YAML file.

For all cases where the message stays unclear, stop the service and start Wings in the foreground. The output is considerably more talkative than the journal:

systemctl stop wings
wings --debug

The built-in diagnostics mode is helpful as well, since it collects configuration, Docker state and system data:

wings diagnostics

Clock drift between Panel and node

This error is treacherous because it looks like a network problem. Symptoms: the node is shown as reachable in the Panel, servers can be created, but the console hangs while connecting, and SFTP access rejects correct credentials.

The reason lies in the design. The Panel signs short-lived tokens whose validity is measured in minutes. Wings checks the time of issue and the expiry against its own clock. If the two systems drift apart by more than a few minutes, Wings discards every token as expired or as not yet valid, even though it was created seconds ago.

Important for understanding this: it is not about the time zone. Panel and node may have different time zones configured, that is irrelevant. It is about the absolute point in time. Check on both machines:

date -u
timedatectl status

In the output of timedatectl, System clock synchronized: yes and NTP service: active have to appear. If they do not:

timedatectl set-ntp true

On systems without systemd-timesyncd, for example after a minimal installation, install chrony and check the synchronization:

apt -y install chrony
chronyc tracking

The value at System time should be in the millisecond range. Virtual machines that were cloned from an image or restored from a snapshot are the most common source of larger deviations.

How you can tell that it really works

A green icon in the Panel is only the first of five proofs. Go through the list and you will know for certain:

  1. systemctl is-active wings returns active, and a journalctl -u wings -n 20 shows no recurring errors.
  2. The node in the overview reports the actual memory and disk capacity of the target server, not just the limits entered in the Panel. Those numbers come live from the node and are proof of working communication.
  3. You create a test server. In the overview it passes through the Installing state and is displayed normally afterwards. On the node, docker ps -a shows the matching container.
  4. You start the server and see live console output in the browser. That is proof of the WebSocket connection, and therefore of the certificate and the clock at the same time.
  5. You connect via SFTP on port 2022 with your panel credentials and see the server files. That confirms the second Wings port as well.

The installation is only complete once all five points are true. Experience shows that points four and five fail most often, even though the Panel looks completely unremarkable up to that moment.

Running Panel and Wings together or separately

Both work. On a single server you only have to keep two things in mind: use two different domain names on the same IP, one for the Panel on port 443 and one for the node on port 8080. And expect the memory appetite of the game servers to slow the Panel down as well when things get tight.

Running them separately is the normal case from the second node onwards anyway, and it has a pleasant side effect: an overloaded or attacked game server does not drag the management interface down with it. For basic hardening of both machines it is worth looking at our checklist for new root servers as well as at the posts on hardening SSH and fail2ban.

One practical note for day-to-day operation to finish with: the Docker images for the game servers bring their own Java environment with them. You do not need to install Java on the node. If you still want to test something outside Pterodactyl at some point, our posts on Java 21 on Debian and on the Minecraft server on Debian show the right way. And keep an eye on disk space, because images and backups grow quickly, which is what the post disk full on Linux is about.

Frequently asked questions

What is the difference between the Pterodactyl Panel and Wings?
The Panel is the PHP web interface with a database that manages users, permissions and configuration. Wings is a separate Go program on the node that talks to Docker and actually starts the game servers. The Panel never runs a game server itself, and Wings has no web interface. The two communicate over HTTP.
Which PHP version do I need for Pterodactyl?
Pterodactyl 1.11 and newer requires PHP 8.2 or 8.3. Debian 12 ships 8.2 and Ubuntu 24.04 ships 8.3, so it works there without third-party repositories. Ubuntu 22.04 only ships 8.1 and Debian 13 already ships 8.4, so in both cases you need an additional PHP repository.
Why does Wings not connect to the Panel?
Check in this order: is the service running (systemctl status wings), is port 8080 open (ss -tlnp), does the panel server reach the node (curl -v https://node1.example.com:8080), does the certificate match the FQDN, and are both clocks in sync. For details, start Wings in the foreground with wings --debug.
Which ports do I have to open for Pterodactyl?
On the panel server, port 443 for HTTPS and port 80 for the certificate renewal. On the node, port 8080 for the Wings communication, port 2022 for SFTP and additionally the port ranges of the game servers themselves, with Minecraft usually from 25565 onwards.
Do I need a separate SSL certificate for every node?
Yes. The Panel addresses the node through its own domain name, and the certificate of the Panel is not enough for that. You either get a separate certificate per node or you use a wildcard certificate for the entire subdomain. Wings has to be restarted after every renewal.
Why does the server console in the browser hang while connecting?
Usually it comes down to the clock. The Panel signs tokens with a very short lifetime and Wings checks them against its own clock. If the system times differ by more than a few minutes, every token is discarded. Check date -u and timedatectl status on both machines. The second common cause is a Panel on HTTPS and a node without SSL.
Do I have to install Java on the node?
No. The Docker images for Minecraft and other Java games bring their own runtime environment with them. Docker alone is enough on the node. A Java installation on the host is not used by Pterodactyl.

Pterodactyl Game server Wings Docker Panel Minecraft Linux Debian Ubuntu