GuidesDevelopers and hostingPterodactyl game panel

Run game servers with Pterodactyl

Pterodactyl on Debian 13, a free panel for game servers such as Minecraft, each in its own Docker container, with the panel and Wings on one server, free certificates, IPv6 for players and backups you can restore.

Tested on Pterodactyl Panel 1.15.1 and Wings 1.13.3 with Caddy 2.6.2 or nginx 1.26.3 and Docker 29.8.1 on Debian 13 (trixie) on a Melonslab server Updated September 27, 2026

Recommended server for this guide

VC-S Micro · 2 vCPU · 8 GB Memory · 250 GB Storage

Month to month, no lock-in 7-day money-back guarantee

€7.99/mo

Deploy now
On this page

What you will set up

Pterodactyl is a panel for game servers. You create a server in your browser, pick a game such as Minecraft, and Pterodactyl installs and runs it in a Docker container of its own, with limits on memory, CPU and disk. You, or the players you give access to, get a console, a file manager, schedules and backups for each server.

Pterodactyl has two parts. The panel is the website, a PHP app with a MariaDB database. Wings runs on each machine that hosts game servers and starts the containers. This guide puts both on the same server. Wings cannot run with rootless Podman: it drives the root Docker daemon directly, creating a container for every game server and changing the ownership of their files, so it runs as root and needs a server of its own. Pelican is a newer fork of Pterodactyl, if you want to compare.

Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:

  • Pterodactyl's documentation lists Debian 13 as supported. The panel ran on Debian's own PHP 8.4, MariaDB 11.8 and Redis 8.0, without the extra package repositories the documentation adds. Step 1 lists what else was adjusted.
  • The admin was created on the command line. Both web servers were tested on the same server: Caddy, and before it nginx with Certbot. Each got Let's Encrypt certificates for the panel and for Wings and sent HTTP to HTTPS. With Caddy, Wings restarted by itself when its certificate was replaced. With Certbot, a test renewal passed.
  • The panel logged each visitor's real address.
  • A location, a node and allocations were created, and a Minecraft server from the default Paper egg was installed and running within a minute. A status ping from outside reached it over IPv4. Over IPv6 it answered from the server itself, as our test machine has no IPv6.
  • The console worked and a backup of the game server was taken and restored with both. The file manager and SFTP were tested with Caddy, also after a reboot.
  • From the internet, only SSH, the website, Wings' ports 8080 and 2022 and the game port were open. The database and Redis were not.
  • The panel and Wings were updated, and everything came back by itself after a reboot, including the game server.

With one idle Minecraft server, the whole server used about 1.5 GB of memory with Caddy and 1.6 GB with nginx, of which the Minecraft server used about 820 MB.

Before you start

You need:

  • a fresh Melonslab server with Debian 13 and nothing else installed. Each game server needs its own memory on top of the panel, so pick the size by the games you want to run;
  • two names with A and AAAA records pointing at the server: one for the panel, such as panel.example.com, and one for Wings, such as wings.example.com.

Set up SSH keys and automatic security updates as in steps 1 to 3 of the security guide, and skip its step 4: step 10 here sets up the firewall with the ports Pterodactyl needs.

The examples use panel.example.com and wings.example.com, and 203.0.113.10 and 2001:db8::10 for the server's addresses. Replace them with your own throughout.

The panel needs a web server, and this guide covers two. Caddy gets and renews certificates by itself. nginx with Certbot is what Pterodactyl's documentation shows first. Pick one, and the steps below follow it:

Web server

1. Install what the panel needs

The panel needs PHP, a web server, MariaDB and Redis. Pterodactyl's guide for Debian adds a package repository for PHP 8.3, and on older Debian releases ones for MariaDB and Redis too. Debian 13 has recent enough versions of all of them. As root:

With Caddy

apt update
apt install -y php8.4 php8.4-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip} mariadb-server caddy tar unzip git redis-server curl composer

With nginx

apt update
apt install -y php8.4 php8.4-{common,cli,gd,mysql,mbstring,bcmath,xml,fpm,curl,zip} mariadb-server nginx tar unzip git redis-server curl composer

This installs PHP 8.4, which Pterodactyl's documentation does not name. The panel's own requirements allow it, and no step below showed an error or warning with it. The other changes from the documentation, for Debian 13 and for this setup, are:

  • the scheduled task in step 4 runs as the web server's user, not as root;
  • .env, which holds the database password and the encryption key, is made readable only by the web server's PHP in step 3;
  • Composer comes from Debian's packages instead of its own installer.

With Caddy

The web server is Caddy from Debian's own package, with Pterodactyl's own Caddy configuration pointed at PHP 8.4 in step 5.

With nginx

The nginx configuration in step 5 is Pterodactyl's own, pointed at PHP 8.4, and also listens on IPv6.

2. Download the panel and create its database

mkdir -p /var/www/pterodactyl
cd /var/www/pterodactyl
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/

Open the database shell with mariadb, and create a user and a database for the panel, with a password of your own:

CREATE USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY 'Choose-a-long-password';
CREATE DATABASE panel;
GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'127.0.0.1' WITH GRANT OPTION;
exit

Then install the panel's PHP libraries and create its encryption key:

cp .env.example .env
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
php artisan key:generate --force

The key is the line starting with APP_KEY= in /var/www/pterodactyl/.env. The panel encrypts secrets such as API keys with it, and a database backup cannot be read without it. Keep a copy of the line somewhere safe, away from the server, such as in a password manager.

3. Configure the panel and create the admin

php artisan p:environment:setup

This asks a few questions. Answer:

QuestionAnswer
Egg Author Emailyour email address
Application URLhttps://panel.example.com
Application Timezoneyour timezone, such as Europe/Stockholm
Cache Driver, Session Driver and Queue Driverredis for all three
Enable UI based settings editor?yes
Enable sending anonymous telemetry data?no, or yes if you want to help

Keep the defaults for the Redis questions. The telemetry is anonymous: once a day, it sends a random ID, the versions of the panel, PHP, the database and Docker, and counts of servers, users and backups to telemetry.pterodactyl.io, with no names, email addresses or IP addresses in the data.

Then connect the panel to its database. Keep the defaults, and enter the password from step 2:

php artisan p:environment:database

Create the tables and load the default eggs, the templates for each game:

php artisan migrate --seed --force

Pterodactyl has no sign-up page for the first admin, so no one else can take the panel before you. Create your admin on the command line:

php artisan p:user:make

Answer yes to Is this user an administrator?, then enter an email address, a username, a first and last name, and a password with at least 8 characters, a capital letter and a number.

Finally, give the files to the web server, and keep .env from other users on the server:

chown -R www-data:www-data /var/www/pterodactyl/*
chown www-data:www-data /var/www/pterodactyl/.env
chmod 600 /var/www/pterodactyl/.env

The panel sends mail for password resets and new users. We did not set it up for this guide. If you have an SMTP server, php artisan p:environment:mail asks for its details. Until then, the panel's log shows an error each time it tries to send.

4. Start the background jobs

The panel runs scheduled tasks every minute and a queue worker for work in the background. Add the scheduled task to the web server's crontab, so that the files it creates belong to the web server:

echo "* * * * * php /var/www/pterodactyl/artisan schedule:run >> /dev/null 2>&1" | crontab -u www-data -

Create /etc/systemd/system/pteroq.service for the queue worker:

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

[Service]
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

Then start it:

systemctl enable --now redis-server pteroq.service

5. Get certificates and set up the web server

With Caddy

Caddy serves the panel, gets its certificate from Let's Encrypt, renews it, and sends HTTP to HTTPS, all by itself. It also gets the certificate for Wings' name, which step 8 points Wings at. Debian's Caddy is version 2.6.2, which is older than Caddy's own releases but understands every line of Pterodactyl's configuration, and it gets security updates from Debian.

Replace /etc/caddy/Caddyfile with this. It is Pterodactyl's own Caddy configuration with PHP 8.4, and a second block at the end that only exists to get the certificate for wings.example.com:

{
    servers :443 {
        timeouts {
            read_body 120s
        }
    }
}

panel.example.com {
    root * /var/www/pterodactyl/public

    file_server

    php_fastcgi unix//run/php/php8.4-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
    }
}

wings.example.com {
    respond 404
}

Then restart Caddy:

systemctl restart caddy

Within a minute, https://panel.example.com answers with a certificate, over IPv4 and IPv6. The Strict-Transport-Security header tells browsers to use only HTTPS for the panel's name for the next six months.

Caddy talks to PHP directly, not as a proxy in front of another web server, so the panel sees each visitor's real address without a trusted proxies setting. Debian's Caddy package adds the caddy user to the www-data group, so it can reach PHP. That is also why .env is readable only by its owner.

With nginx

The panel and Wings each need a certificate for their own name. Wings is restarted when its certificate renews, which does not stop the game servers:

apt install -y certbot python3-certbot-nginx
certbot certonly --nginx -d panel.example.com
certbot certonly --nginx -d wings.example.com --deploy-hook "systemctl restart wings"

Certbot asks for an email address for expiry notices, and to accept Let's Encrypt's terms. Debian's certbot renews the certificates by itself. certbot renew --dry-run tests it.

Remove nginx's default site:

rm /etc/nginx/sites-enabled/default

Create /etc/nginx/sites-available/pterodactyl.conf. This is Pterodactyl's own configuration, with IPv6 added and PHP 8.4:

server {
    listen 80;
    listen [::]:80;
    server_name panel.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name panel.example.com;

    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_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/panel.example.com/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;

    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.4-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;
    }
}

Enable it:

ln -s /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/pterodactyl.conf
nginx -t
systemctl restart nginx

nginx passes requests to PHP directly, so the panel sees each visitor's real address without a trusted proxies setting.

6. Log in and switch off reCAPTCHA

Open https://panel.example.com and log in with the admin from step 3. The cog icon at the top right opens the admin area, at https://panel.example.com/admin.

By default, the login page uses Google's reCAPTCHA, with keys that ship with every copy of Pterodactyl, so every visitor's browser loads it from Google. Logins are limited without it too: a few failed attempts lock the login for a while. To switch it off, open Settings, then Advanced, set Status under reCAPTCHA to Disabled, and choose Save. If you want to keep it, create keys of your own with Google and enter them there.

Under Settings and General, Require 2-Factor Authentication can be set to Admin Only or All Users.

7. Install Docker and Wings

Install Docker with Docker's own script, as Pterodactyl's documentation does. It sets Docker to start at boot:

curl -sSL https://get.docker.com/ | CHANNEL=stable bash

Then download Wings:

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

Create /etc/systemd/system/wings.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. It needs its configuration from the panel first.

8. Create a location and a node

A node is a machine that runs Wings, and every node belongs to a location. In the admin area:

  • Open Locations and choose Create New. Enter a Short Code, such as se-sto, and a Description, such as Stockholm, and choose Create.
  • Open Nodes and choose Create New. Enter a Name, pick the Location, and enter wings.example.com as the FQDN. Keep Use SSL Connection and Not Behind Proxy.
  • Under Configuration, enter how much memory and disk the game servers may use in total, in MiB, such as 6144 for Total Memory and 30720 for Total Disk Space, and 0 for both over-allocations. Leave some memory for the panel. Keep Daemon Port 8080 and Daemon SFTP Port 2022, and choose Create Node.

Open the node's Configuration tab, and choose Generate Token. The panel shows a command starting with cd /etc/pterodactyl && sudo wings configure. Run it on the server. As root, you can leave out sudo. It writes /etc/pterodactyl/config.yml and ends with Successfully configured wings.

With Caddy

The file expects the certificate where Certbot would put it, under /etc/letsencrypt. Point it at Caddy's certificate for wings.example.com instead. Wings runs as root, so it can read Caddy's files:

C=/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wings.example.com
sed -i "s#cert: /etc/letsencrypt/live/wings.example.com/fullchain.pem#cert: $C/wings.example.com.crt#; s#key: /etc/letsencrypt/live/wings.example.com/privkey.pem#key: $C/wings.example.com.key#" /etc/pterodactyl/config.yml
grep -A4 "ssl:" /etc/pterodactyl/config.yml

The last command shows the two new paths under cert: and key:. When you change the node in the panel later, Wings keeps them.

Wings reads the certificate only when it starts, and Caddy renews it about a month before it expires. So have systemd restart Wings whenever Caddy writes a new one. Create /etc/systemd/system/wings-cert.path:

[Unit]
Description=Restart Wings when Caddy renews its certificate

[Path]
PathChanged=/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wings.example.com/wings.example.com.crt

[Install]
WantedBy=multi-user.target

And /etc/systemd/system/wings-cert.service, which waits ten seconds so that Caddy has written the key too:

[Unit]
Description=Restart Wings for a renewed certificate

[Service]
ExecStartPre=/bin/sleep 10
Type=oneshot
ExecStart=/usr/bin/systemctl restart wings.service

Restarting Wings does not stop the game servers. Then start Wings and the watch:

systemctl daemon-reload
systemctl enable --now wings wings-cert.path

With nginx

The file already points Wings at the certificate for wings.example.com from step 5. Certbot renews it and restarts Wings with the deploy hook from step 5. Start Wings:

systemctl enable --now wings

Restarting Wings does not stop the game servers.

The token is an API key with access to your nodes, and the command also prints it in full. Wings does not need it once it is configured, so delete it: open Application API, and remove the key with the memo Automatically generated node deployment key.

9. Add allocations

An allocation is an address and a port that a game server can use. Open the node's Allocation tab. Under Assign New Allocations:

  • Enter 0.0.0.0 as the IP Address, 25565 in Ports, and a name that points at the server as the IP Alias, such as wings.example.com, and choose Submit. The alias is the address players see in the panel.
  • Add the same port again with :: as the IP Address, for IPv6.

0.0.0.0 accepts players on every IPv4 address of the server and :: on every IPv6 address. Ports takes a range too, such as 25565-25570, for more servers.

10. Open the firewall

Wings' API on port 8080 is what your browser connects to for the console, and port 2022 is its SFTP server for game server files. Both need to be reachable, as do SSH and the website.

With Caddy

Caddy also answers on UDP port 443 for HTTP/3:

apt install -y ufw
ufw allow 22/tcp
ufw allow 80,443/tcp
ufw allow 443/udp
ufw allow 8080,2022/tcp
ufw enable

With nginx

apt install -y ufw
ufw allow 22/tcp
ufw allow 80,443/tcp
ufw allow 8080,2022/tcp
ufw enable

Game ports need no rule. Docker opens the ports it publishes past ufw, so a game port is open to the internet as soon as a server uses its allocation, and closed again when no server does. Ports without a server stay closed. MariaDB and Redis only listen on the server itself.

11. Create a game server

In the admin area, open Servers and choose Create New:

  • Enter a Server Name, and type your email address in Server Owner to pick your account.
  • Pick the node, and 0.0.0.0:25565 as the Default Allocation.
  • Under Application Feature Limits, set Backup Limit to the number of backups to keep, such as 2. It starts at 0, which means no backups for this server.
  • Set Memory and Disk Space in MiB, such as 2048 and 5120.
  • Under Nest Configuration, pick Minecraft and the Paper egg. The defaults under Docker Configuration and Service Variables install the latest Minecraft version.
  • Choose Create Server.

For IPv6, open the server's Build Configuration tab, pick :::25565 under Assign Additional Ports, and choose Update Build Configuration.

Then open the server from your own list at https://panel.example.com. The install took seconds on our test server. Choose Start. Minecraft stops at first until you accept its licence, and the panel asks you to: read it, and choose I Accept. The server starts, and the console shows its output.

Players connect to wings.example.com, the address shown under Address. Minecraft's default port, 25565, does not need to be typed.

12. Back up

On the server's Backups tab, choose Create backup, give it a Backup name, and choose Start backup. Our Minecraft server's backup was 208 MB. To restore, open the menu next to a backup, choose Restore, and tick Delete all files before restoring backup to get exactly the backed up files back. The server stops while it restores. Schedules can create backups by themselves.

These backups are saved in /var/lib/pterodactyl/backups on the same server, so they do not help if the server is lost. Pterodactyl can also store them in an S3 bucket, which we did not test. To rebuild the panel itself, keep a copy of these away from the server:

  • a dump of the database, from mariadb-dump --single-transaction panel | gzip > panel.sql.gz;
  • /var/www/pterodactyl/.env, with the encryption key;
  • /etc/pterodactyl/config.yml;
  • the game servers' files in /var/lib/pterodactyl/volumes, or their backups.

13. Updates

Check Pterodactyl's releases before you update. To update the panel:

cd /var/www/pterodactyl
php artisan down
curl -L https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz | tar -xzv
chmod -R 755 storage/* bootstrap/cache
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
php artisan view:clear
php artisan config:clear
php artisan migrate --seed --force
chown -R www-data:www-data /var/www/pterodactyl/*
php artisan queue:restart
php artisan up

To update Wings, stop it, download it again, and start it. Game servers keep running while Wings is stopped:

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
systemctl restart wings

PHP, the web server, MariaDB, Redis and Docker are Debian and Docker packages, covered by the automatic security updates from the security guide.

Troubleshooting

The console says it cannot connect, or shows no stats. Your browser connects to Wings at wings.example.com on port 8080. Check that Wings runs with systemctl status wings, that port 8080 is allowed in ufw, and that the node's FQDN matches the name on Wings' certificate. journalctl -u wings shows why Wings stopped.

With Caddy

Wings does not start, and journalctl -u wings shows failed to configure HTTPS server with no such file or directory. Caddy has not got the certificate for wings.example.com yet, usually because its A or AAAA record does not point at the server. journalctl -u caddy shows why. Once the file exists in the directory from step 8, systemctl restart wings starts Wings.

With nginx

Wings does not start, and journalctl -u wings shows failed to configure HTTPS server with no such file or directory. Certbot has not got the certificate for wings.example.com, usually because its A or AAAA record does not point at the server. Run its certbot certonly command from step 5 again, which shows why it fails, then systemctl restart wings.

Players cannot connect over IPv6. The server has no allocation on ::. Add one as in step 9, assign it as in step 11, and restart the game server from its console.

The Backups tab says "Backups cannot be created for this server because the backup limit is set to 0." Raise Backup Limit on the server's Build Configuration tab in the admin area, and choose Update Build Configuration.

Run it on your own server

VC-S Micro

€7.99/mo

vCPU
2
Memory
8 GB
Storage
250 GB
Transfer
10 TB
Standard
HDD · RAID 10
  • Full root access
  • Native /64 IPv6
  • RAID-protected storage
  • Malmö, Sweden
  • Month to month, no lock-in
  • 7-day money-back guarantee
All guides