We'll install Pterodactyl Panel, a web interface for managing game servers, on your VPS. When we're done, it will run at https://panel.example.cz with a Let's Encrypt certificate, a job queue, cron and your admin account.
What you'll need
- A StormHost VPS with a fresh installation of Ubuntu 24.04 or Debian 13
- A domain or subdomain for the panel (
panel.example.czin this guide) and access to its DNS settings - An SSH connection as root (guide)
- Basic terminal skills: editing files in the
nanoeditor, copying commands - About 90 minutes
How it works#
Pterodactyl has two parts. The Panel is a web application (PHP, Laravel framework) where you create users and servers and where players control the console. Wings is the service that actually runs the game servers in Docker containers. Panel and Wings can run on the same VPS. This guide installs the Panel; you'll add Wings in the follow-up guide Pterodactyl Wings.
The Panel needs several other programs:
| Component | What the Panel needs it for |
|---|---|
| PHP 8.3 + PHP-FPM | The Panel is written in PHP. PHP-FPM is the process that runs PHP code for the web server. |
| MariaDB | The database. Stores users, servers, settings and game templates. |
| Redis | Fast in-memory storage for the cache, logins (sessions) and the job queue. |
| Nginx | The web server. Accepts requests from the browser and passes them to PHP-FPM. |
| Composer | The PHP library manager. Downloads the libraries the Panel needs. |
| Cron and the pteroq service | Cron runs the Panel's scheduler every minute; pteroq processes the job queue in the background (emails, scheduled server tasks). |
The Panel itself is lightweight. If game servers will run on the same VPS, plan your RAM mainly around them and add a reserve for the system, the database and the Panel (roughly 1–2 GB).
Do you really need your own panel?
Pterodactyl pays off when you want to run several game servers on one VPS, manage them through the web and give other people access. If one game server is enough for you, you can order it directly as a game server and manage it in the customer area without installing anything.
Installation plan#
| Phase | What you'll do |
|---|---|
| 1. Domain | Point panel.example.cz at the VPS IP address. |
| 2. Firewall | Open ports 80 and 443. |
| 3. Dependencies | Install PHP 8.3, MariaDB, Redis, Nginx and Composer. |
| 4. Panel | Download the Panel files, create the database and set up .env. |
| 5. Database and admin | Create the tables and the first administrator. |
| 6. Background | Set up cron and the pteroq queue service. |
| 7. HTTPS and Nginx | Get a certificate and configure the web server. |
| 8. Check | Log in and verify that everything runs. |
Domain and DNS#
The Panel needs its own domain or subdomain. HTTPS certificates are issued for domains, not for IP addresses.
In the customer area, open My services, click your VPS and copy the IPv4 address (and optionally the IPv6 address) in the Network tab.
At your DNS provider, create an
Arecord forpanel.example.czwith the VPS's IPv4 address (for example203.0.113.10).Optionally, also create an
AAAArecord with the VPS's IPv6 address (for example2001:db8::10). The Nginx configuration below listens on IPv6 too.On the VPS, check that the domain already points to the right place:
bash getent hosts panel.example.czgetent hosts– asks the system DNS for the domain's address and prints it.
You'll see a line with your VPS's IP address. If nothing is printed, wait a few minutes (depending on the record's TTL) and try again.
Cloudflare: gray cloud only during installation
If your domain is managed in Cloudflare, keep the record set to DNS only (gray cloud). That way you can verify the certificate and the first login directly. You can switch to Proxied (orange cloud) at the end; see Panel behind Cloudflare. Cloudflare setup: Domain on a VPS and Cloudflare.
Firewall#
The browser connects to the Panel on port 443 (HTTPS). Port 80 (HTTP) is needed for domain validation when issuing the certificate and for redirecting to HTTPS.
Firewall in the customer area: if inbound traffic is set to Block everything except allowed, add the rules Inbound · Allow · TCP · port
80,443. The SSH rule (port22) must stay. Guide: Firewall in the customer area.UFW on the VPS: if you use it, allow the ports there too:
bash ufw allow 80/tcp ufw allow 443/tcpufw allow 80/tcp– allows incoming TCP connections to port 80.ufw allow 443/tcp– the same for port 443.
You'll open the ports for Wings (8080 and 2022) and for game servers in the Pterodactyl Wings guide.
Installing dependencies#
First update the system so you install on an up-to-date base:
apt update && apt -y upgradeapt update– downloads the current package list.apt -y upgrade– installs available updates;-yanswers “yes” to the confirmation.
Then install the dependencies. The steps depend on the system: Ubuntu 24.04 has PHP 8.3 directly in its repository, while Debian 13's default repository has PHP 8.4, which the Panel doesn't support (the current Panel requires PHP 8.2 or 8.3). On Debian you'll therefore add the packages.sury.org repository, which the official Pterodactyl documentation recommends as well.
Ubuntu 24.04
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 curlDon't add any PHP repository on Ubuntu 24.04. The official documentation only mentions the ppa:ondrej/php repository for the older Ubuntu 22.04.
Debian 13
First add the repository with PHP 8.3:
apt -y install lsb-release ca-certificates curl
curl -sSLo /tmp/debsuryorg-archive-keyring.deb https://packages.sury.org/debsuryorg-archive-keyring.deb
dpkg -i /tmp/debsuryorg-archive-keyring.deb
echo "deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list
apt updateapt -y install lsb-release ca-certificates curl– a tool for finding the system's release name, certificates for HTTPS and thecurldownload tool.curl -sSLo … debsuryorg-archive-keyring.– downloads the package with the repository's signing key (deb -ssilent,-Sshow errors,-Lfollow redirects,-osave to a file).dpkg -i …– installs the key into/usr/share/keyrings/; apt then uses it to verify packages from the repository.echo "deb …" > /etc/– writes the repository address.apt/ sources. list. d/ php. list $(lsb_release -sc)is replaced with the system's codename (trixie).apt update– loads the packages from the new repository.
Then install the 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-serverOn Debian 13, install MariaDB and Redis from the default repository; they don't need any extra repository.
What the install command contains:
| Package | What it's for |
|---|---|
php8.3, php8.3-cli | PHP 8.3 and its command line (for the php artisan commands). |
php8.3-fpm | The process Nginx uses to run PHP. Listens on the socket /run/php/php8.3-fpm.sock. |
php8.3-common, -gd, -mysql, -mbstring, -bcmath, -xml, -curl, -zip | PHP extensions the Panel requires: images, MariaDB connections, UTF-8 text handling, precise math, XML, HTTP requests and ZIP archives. The php8.3-{common,cli,…} notation is a bash shortcut that expands to php8.3-common php8.3-cli …. |
mariadb-server | The database server. |
nginx | The web server. |
redis-server | Redis. |
tar, unzip, git, curl | Unpacking archives and downloading; Composer uses them when installing libraries. |
Check that you have the right PHP version:
php -vPHP 8.3.x (cli) …The first line must say PHP 8.3. If you see PHP 8.4, see Troubleshooting.
Check that the services are running:
systemctl is-active mariadb redis-server nginx php8.3-fpmsystemctl is-active– prints the state of each listed service.
You'll see active four times. If any of them is inactive, start it and enable it at boot with systemctl enable --now <SERVICE_NAME>, for example systemctl enable --now redis-server.
Composer#
Composer downloads the Panel's PHP libraries. Install it with the official installer:
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composercurl -sS https://– downloads the Composer install script.getcomposer. org/ installer | php --– passes the script to PHP to run;--separates PHP's parameters from the installer's.--install-dir=– where Composer gets installed (a directory where the system looks for programs)./usr/ local/ bin --filename=composer– the program name, so you then run it with thecomposercommand.
Check: composer --version prints Composer version 2.…. As root you'll also see the warning Do not run Composer as root/super user! and possibly the question Continue as root/super user [yes]?. Confirm it with Enter. During the Panel installation below, the COMPOSER_ variable suppresses the warning.
Downloading the Panel#
Create a directory for the Panel and switch to it:
bash mkdir -p /var/www/pterodactyl cd /var/www/pterodactylmkdir -p– creates the directory (including its parents); if it already exists, it doesn't print an error.cd– switches to the directory. Run all furtherphp artisancommands in it.
Download and unpack the latest Panel release:
bash curl -Lo panel.tar.gz https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz tar -xzvf panel.tar.gzcurl -Lo panel.tar.gz …– downloads the latest release archive from GitHub and saves it aspanel.tar.gz.tar -xzvf– unpacks the archive (xextract,zgzip format,vlist files,ffrom a file).
Set permissions on the directories the Panel writes to:
bash chmod -R 755 storage/* bootstrap/cache/chmod -R 755– the owner can read, write and enter the directories; everyone else can only read and enter them.-Rapplies to all contents.storage/– logs, cache and uploaded files;bootstrap/cache/– the compiled configuration.
A database for the Panel#
The Panel needs its own database and its own user. The Pterodactyl authors explicitly forbid using the database user root for the Panel: if someone compromised the Panel, they would get access to all databases on the VPS.
Generate a strong password for the database user, for example with the password generator. Use only letters and digits (at least 24 characters) so you don't have to deal with special characters in SQL and in
.env. Save the password; you'll need it again shortly.Log in to MariaDB as the administrator:
bash mariadbmariadb– the database client. On both Ubuntu and Debian, the systemrootuser logs in to MariaDB without a password (via unix socket authentication). You'll see theMariaDB [(none)]>prompt.
Create the user and the database. Replace
<PANEL_DB_PASSWORD>with the password from step 1 (keep the single quotes around it):SQL CREATE USER 'pterodactyl'@'127.0.0.1' IDENTIFIED BY '<PANEL_DB_PASSWORD>'; CREATE DATABASE panel; GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'127.0.0.1'; exit
| Line | What it does |
|---|---|
CREATE USER 'pterodactyl'@'127. | Creates the user pterodactyl, who may only log in from 127.0.0.1, that is, only from this VPS over TCP. Nobody can connect with this account from outside. |
IDENTIFIED BY '…' | Sets the user's password. |
CREATE DATABASE panel; | Creates an empty panel database. |
GRANT ALL PRIVILEGES ON panel.* … | Gives the user all privileges, but only on the panel database (panel.* = all its tables). It has no access to other databases. |
exit | Exits the MariaDB client. |
Why 127.0.0.1 and not localhost
The Panel connects to the database over TCP at 127.0.0.1. The Pterodactyl authors explicitly advise against using localhost, because PHP then tries a socket connection, which causes problems. So the database user must allow host 127.0.0.1.
Environment configuration (.env)#
The Panel's settings live in /var/. You'll create it from a template, install the libraries and generate an encryption key.
Copy the template:
bash cp .env.example .envInstall the Panel's PHP libraries:
bash COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloaderCOMPOSER_– lets Composer run as root without asking.ALLOW_ SUPERUSER=1 composer install– downloads the libraries in exactly the versions the Panel specifies (thecomposer.lockfile).--no-dev– skips developer tools you don't need on a production server.--optimize-autoloader– prepares faster loading of PHP classes.
The installation takes a few minutes and finishes without error messages (a yellow warning about deprecated packages is fine).
Generate the encryption key:
bash php artisan key:generate --forcephp artisan– the Panel's command-line management tool.key:generate– creates a randomAPP_KEYand writes it to.env.--force– runs the command without confirmation (the Panel runs in production mode, where it would otherwise ask).
Back up APP_KEY right now
The Panel uses APP_KEY to encrypt sensitive data in the database (for example API keys and database host passwords). If you lose it, you can't decrypt the data, not even from a database backup. Only run key:generate on the first installation. On a Panel that already has data, a new key would mean losing access to it.
Display the key and save the whole line somewhere off the server (in a password manager or an encrypted file on your computer):
bash grep APP_KEY /var/www/pterodactyl/.envOutputtext APP_KEY=base64:…
The p:environment:setup wizard#
A wizard takes care of the basic setup. Run it:
php artisan p:environment:setupIt asks the following questions in turn (the default value is in square brackets; confirm it with Enter):
| Question | What to enter | Why |
|---|---|---|
Author email (… Author Email) | Your email, for example [email protected] | Used as the author of game templates you export from the Panel. It has to be a valid email format, otherwise the wizard fails. |
Application URL | https://panel.example.cz | The Panel's address including https://. Used for links in emails and when generating URLs. Without https:// the links will be wrong. |
Application Timezone | Europe/Prague | The time zone for displaying times and for scheduled tasks. |
Cache Driver | redis | Where to store the cache. Redis is the fastest and the recommended option. |
Session Driver | redis | Where to store user logins. |
Queue Driver | redis | Where to store the job queue for the pteroq service. |
Enable UI based settings editor? | yes | Some settings (Panel name, mail, reCAPTCHA) can then be changed in the admin area, not just in .env. |
Enable sending anonymous telemetry data? | your call | Once a day the Panel sends anonymous statistics to its authors (number of servers, system version). If you don't want that, answer no. |
Redis Host | 127.0.0.1 | Redis runs on the same VPS. |
Redis Password | nothing, just Enter | A local Redis has no password after installation and only listens on 127.0.0.1. |
Redis Port | 6379 | The default Redis port. |
When you enter an address starting with https://, the wizard turns on secure cookies by itself (SESSION_).
The p:environment:database wizard#
php artisan p:environment:database| Question | What to enter | Why |
|---|---|---|
Database Host | 127.0.0.1 | The database runs on the same VPS. Don't enter localhost (see above). |
Database Port | 3306 | The default MariaDB port. |
Database Name | panel | The database from the previous section. |
Database Username | pterodactyl | The user from the previous section. |
Database Password | the <PANEL_DB_PASSWORD> password | It isn't shown while typing. |
The wizard tries to connect right away. If the details are wrong, it prints Unable to connect to the MySQL server…, saves nothing and asks whether you want to try again.
The p:environment:mail wizard#
The Panel sends emails (invitations for new users, password resets). Without mail settings the Panel works, but these emails won't go out. You can also set this up later with the same command:
php artisan p:environment:mail| Question | What to enter | Why |
|---|---|---|
Which driver should be used for sending emails? | smtp | Sending through your email provider's SMTP server is the most reliable. |
SMTP Host | the SMTP server address, for example smtp.example.cz | You'll find it in your email provider's help pages. |
SMTP Port | 587 (or 465) | 587 is used with TLS encryption (STARTTLS), 465 with SSL encryption. |
SMTP Username | the mailbox's full email address | The mailbox login name. |
SMTP Password | the mailbox password | Some services require a separate “app password”. |
Encryption method to use | tls for port 587, ssl for 465 | Must match the port, otherwise the connection fails. |
Email address emails should originate from | for example [email protected] | The sender address. It should belong to the mailbox's domain, otherwise messages end up in spam. |
Name that emails should appear from | for example Game Panel | The sender name shown in emails. |
Important keys in .env#
The wizards write these values to .env. You can also edit the file by hand (nano /var/); after a manual edit, run php artisan config:clear.
| Setting | What it does | Recommended value | When to change |
|---|---|---|---|
APP_URL | The Panel's public address. | https://panel.example.cz | When the domain changes (then also in Nginx and the certificate). |
APP_KEY | The encryption key for data in the database. | generated, never change it | Never. When restoring from a backup, put the original back. |
APP_TIMEZONE | The Panel's time zone. | Europe/Prague | When the admins are in a different time zone. |
APP_ENVIRONMENT_ONLY | true = settings only in .env, false = also in the admin area. | false | When you want to prevent settings changes from the web. |
DB_HOST, DB_PORT | Where the Panel's database runs. | 127.0.0.1, 3306 | When you move the database elsewhere. |
DB_DATABASE, DB_USERNAME, DB_PASSWORD | The Panel's database and credentials. | panel, pterodactyl, a strong password | When you change the user's password in MariaDB. |
REDIS_HOST, REDIS_PASSWORD, REDIS_PORT | The Redis connection. | 127.0.0.1, null, 6379 | When you set a Redis password. |
CACHE_DRIVER, SESSION_DRIVER, QUEUE_CONNECTION | Where the cache, logins and queue are stored. | redis | Practically never. |
SESSION_SECURE_COOKIE | Login cookies are only sent over HTTPS. | true | Never, as long as the Panel runs on HTTPS. |
MAIL_* | Sending email (server, port, encryption, sender). | depends on the provider | When you change the mailbox. |
MAIL_EHLO_DOMAIN | The name the Panel introduces itself with to the SMTP server. | panel.example.cz | When, for example, Gmail rejects your emails because of localhost. |
TRUSTED_PROXIES | Proxy addresses the Panel trusts (Cloudflare, another Nginx). | empty | When the Panel runs behind a proxy; see Panel behind Cloudflare. |
PTERODACTYL_ | Sending anonymous telemetry. | your call | false turns telemetry off. |
Database tables and the first administrator#
Create the tables and load the default data:
bash php artisan migrate --seed --forcemigrate– creates all tables in thepaneldatabase.--seed– fills them with default data, including game templates (Minecraft, Rust and others).--force– runs the command without confirmation in production mode.
The command can take several minutes. Don't interrupt it, otherwise the database stays half-created.
Create the admin account:
bash php artisan p:user:make
| Question | What to enter |
|---|---|
Is this user an administrator? | yes – the account gets access to the admin area. |
Email Address | Your email; used for logging in and password resets. |
Username | The login name, for example jan. |
First Name, Last Name | First and last name (shown in the Panel). |
Password | At least 8 characters, at least one uppercase letter and one digit. It isn't shown while typing. |
At the end you'll see a table with the account details and the row Admin | Yes.
File permissions#
The Panel files now belong to root. The web server runs as the www-data user and has to write to the storage and bootstrap/cache directories:
chown -R www-data:www-data /var/www/pterodactyl/*chown -R– changes the owner of the files and the entire contents of directories.www-data:www-data– the user and group Nginx and PHP-FPM run as on Ubuntu and Debian.
After every artisan command run as root
php artisan commands run as root can create files (for example logs) that www-data then can't overwrite. If you get a 500 error after working in the terminal, run the chown command above again.
Cron and the job queue#
Cron#
Cron runs the Panel's scheduler every minute (cleaning up old logins, scheduled server tasks).
Open root's crontab:
bash crontab -eOn the first run it asks which editor to use. Choose
nano(type its number and press Enter). If the system sayscrontab: command not found, cron is missing. Install it withapt -y install cronand repeat this step.Add this line at the end of the file:
text * * * * * php /var/www/pterodactyl/artisan schedule:run >> /dev/null 2>&1Save (Ctrl+O, Enter) and close the editor (Ctrl+X). You'll see
crontab: installing new crontab.
| Part of the line | Meaning |
|---|---|
* * * * * | Minute, hour, day of month, month, day of week. Five asterisks = every minute. |
php /var/ | Runs the Panel's scheduler, which decides by itself which tasks are due. |
>> /dev/null 2>&1 | Discards output and errors so cron doesn't send an email every minute. |
The pteroq queue service#
The queue handles background work: sending emails and the tasks users schedule for their servers (the Schedules section, for example a nightly restart or backup). Without it, emails don't go out and scheduled tasks don't run.
Create the service file:
bash nano /etc/systemd/system/pteroq.servicePaste the content:
/etc/ systemd/ inisystem/ pteroq.service # Pterodactyl Queue Worker File [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.targetSave and close the editor.
| Line | What it does |
|---|---|
Description= | The service description you'll see in systemctl status. |
After= | The service starts only after Redis, which it reads the queue from. |
User=www-data, Group=www-data | The queue runs as the same user as the website, so both have the same file permissions. |
Restart=always | When the process exits (with an error or normally), systemd starts it again. |
ExecStart=… queue:work | Starts processing the queue. --queue= sets the queue order by priority, --sleep=3 = wait 3 seconds when the queue is empty, --tries=3 = retry a failed job at most three times. |
StartLimitInterval=180, StartLimitBurst=30 | If the service crashes more than 30 times in 180 seconds, systemd stops restarting it (protection against an endless loop). |
RestartSec=5s | Waits 5 seconds before restarting. |
WantedBy= | The service starts at every VPS boot (after enable). |
Enable Redis and the queue at boot and start them right away:
bash systemctl enable --now redis-server systemctl enable --now pteroq.serviceenable– turns on automatic start after a VPS reboot.--now– also starts the service immediately.
Check the status:
bash systemctl status pteroqYou'll see
Active: active (running). Close the output with q.
The Let's Encrypt certificate#
The Nginx configuration below refers to the certificate files. Nginx won't start with it until the certificate exists, so you get the certificate first.
Install Certbot and its Nginx plugin:
bash apt -y install certbot python3-certbot-nginxcertbot– the program that gets and renews Let's Encrypt certificates.python3-certbot-nginx– a plugin that verifies the domain through the running Nginx without stopping it.
Get the certificate. Replace
panel.example.czwith your domain:bash certbot certonly --nginx -d panel.example.czcertonly– only gets the certificate and saves it, without editing the Nginx configuration (you'll write that yourself).--nginx– domain validation goes through Nginx on port 80.-d panel.example.cz– the domain you want the certificate for.
On the first run, answer the questions:
- Email (
Enter email address…) – enter your email. Let's Encrypt sends important account notices there. Since 2025 it no longer sends certificate expiry reminders, so renewal is handled automatically (see step 5). - Terms of Service (
Please read the Terms of Service… Do you agree?) – answerY; without agreeing you won't get a certificate. - Sharing your email with the EFF (
Would you be willing… to share your email address with the Electronic Frontier Foundation…) – your call; it has no effect on the certificate.
At the end you'll see
Successfully received certificate.and the file paths:Outputtext Certificate is saved at: /etc/letsencrypt/live/panel.example.cz/fullchain.pem Key is saved at: /etc/letsencrypt/live/panel.example.cz/privkey.pem- Email (
Make Nginx load the new certificate after every renewal. Create a deploy hook (a script Certbot runs after a successful renewal):
bash cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF' #!/bin/sh systemctl reload nginx EOF chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shcat > … <<'EOF' … EOF– writes the lines between<<'EOF'andEOFto the file.systemctl reload nginx– loads the new configuration and certificates without dropping connections.chmod 755– makes the script executable; Certbot only runs executable files in therenewal-hooks/deploydirectory.
Check that automatic renewal works. On both Ubuntu and Debian, the Certbot package includes a timer that tries renewal twice a day:
bash systemctl list-timers | grep certbot certbot renew --dry-runsystemctl list-timers– lists scheduled timers;certbot.timershould be among them.certbot renew --dry-run– tries a renewal against the Let's Encrypt staging server without touching the real certificate.
The test ends with
Congratulations, all simulated renewals succeeded.
The certificate is valid for 90 days (as of October 2026; Let's Encrypt is gradually shortening the validity). Certbot renews it by itself when roughly a third of its validity is left.
Nginx#
Disable Nginx's default website:
bash rm /etc/nginx/sites-enabled/default- This only deletes the link in
sites-enabled; the file itself stays insites-available. Otherwise the default website would serve port 80 instead of the Panel.
- This only deletes the link in
Create the Panel configuration:
bash nano /etc/nginx/sites-available/pterodactyl.confPaste the content and replace all four occurrences of
panel.example.czwith your domain:/etc/ nginx/ nginxsites-available/ pterodactyl.conf server { listen 80; listen [::]:80; server_name panel.example.cz; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name panel.example.cz; server_tokens off; 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; # larger uploads and longer script run times client_max_body_size 100m; client_body_timeout 120s; sendfile off; ssl_certificate /etc/letsencrypt/live/panel.example.cz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/panel.example.cz/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.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; } }
The configuration is based on the official Pterodactyl documentation. We added IPv6 listening (listen [::]:…) and hid the Nginx version. What each part does:
| Directive | What it does | When to change |
|---|---|---|
server_tokens off; | Nginx doesn't reveal its version in headers and on the Panel's error pages. On Debian 13 this is already the default in /etc/nginx/nginx.conf, on Ubuntu 24.04 it isn't. | Never. |
first server block with listen 80 | Accepts HTTP requests and permanently (301) redirects them to HTTPS. | Never. |
listen 443 ssl http2; | Listens on port 443 with TLS and HTTP/2. [::] is the same for IPv6. | Delete the lines with [::] if the VPS has no IPv6. |
server_name | The domain this block applies to. | When the domain changes. |
root /var/ | The directory files are served from. Only public; the Panel's other files (including .env) aren't reachable from the web. | Never. |
index index.php; | Which file to use when the URL ends with /. | Never. |
access_log, error_log | Where the access and error logs are written. | Never. |
client_max_body_size 100m; | The maximum allowed upload size (for example through the Panel's file manager). | When you need to upload larger files; also change upload_max_filesize and post_max_size below. |
client_body_timeout 120s; | How long Nginx waits for more data from the browser during an upload, so a slow upload of a large file isn't cut off. | When uploading over a very slow connection. |
sendfile off; | Turns off sending files directly by the kernel. Taken from the official Pterodactyl configuration. | Never. |
ssl_certificate, ssl_certificate_key | Paths to the certificate and private key from Certbot. | When the domain changes. |
ssl_session_cache shared:SSL:10m; | Memory (10 MB) for stored TLS sessions, so repeat browser connections are set up faster. | Never. |
ssl_protocols TLSv1.2 TLSv1.3; | Allowed TLS versions; older and insecure ones are off. | Never. |
ssl_ciphers, ssl_ | Allowed ciphers and their order. | Never. |
add_header … | Security headers: prevents embedding the Panel in other pages, blocks indexing by search engines and similar. | Never. |
location / + try_files | Sends existing files directly and passes everything else to index.php (the application). | Never. |
location ~ \.php$ | Rules for all requests to .php files. | Never. |
fastcgi_pass unix:/ | How Nginx passes PHP requests to PHP-FPM. | When switching to a different PHP version. |
fastcgi_split_path_info, fastcgi_index, include fastcgi_params, fastcgi_param SCRIPT_FILENAME | Standard request handoff to PHP: splits the URL into the script and the rest of the path, sets the default script and passes the request variables to PHP, including the path to the script being run. | Never. |
fastcgi_intercept_errors off; | Error pages come straight from the Panel; Nginx doesn't replace them with its own. | Never. |
fastcgi_buffer_size, fastcgi_buffers | The size of the memory Nginx uses to buffer responses from PHP. | Never. |
fastcgi_param PHP_VALUE … | Raises PHP's upload limits to 100 MB. | Together with client_max_body_size. |
fastcgi_param HTTP_PROXY ""; | Protection against the “httpoxy” attack. | Never. |
fastcgi_*_timeout 300 | How long Nginx waits for PHP (slow operations). | Never. |
location ~ /\.ht + deny all | Blocks access to hidden .ht… files. | Never. |
http2 warning on Debian 13
Debian 13 ships Nginx 1.26, where the listen … http2 syntax is marked as deprecated. The configuration test prints the warning the "listen ... http2" directive is deprecated. It doesn't affect anything. If the warning bothers you, on Debian 13 remove the word http2 from both listen 443… lines and add the line http2 on; below them. On Ubuntu 24.04 (Nginx 1.24) the http2 on; directive doesn't exist, so keep the syntax from the example there.
Enable the configuration, test it and restart Nginx:
bash ln -s /etc/nginx/sites-available/pterodactyl.conf /etc/nginx/sites-enabled/pterodactyl.conf nginx -t systemctl restart nginxln -s– creates a link to the file insites-enabled; Nginx only loads configurations insites-enabled.nginx -t– checks the syntax. It must end with the linetest is successful. If it doesn't, don't restart Nginx; fix the line the error points to.systemctl restart nginx– loads the new configuration.
First login#
- Open
https://panel.example.czin your browser. You'll see the Panel's login page with a padlock (valid certificate) in the address bar. - Log in with the admin's username or email and password.
- Click the gear icon (admin area) in the top right. In the Settings section, set the Panel name (Company Name).
- Turn on two-factor authentication: click your account in the top right, open Account and in the two-factor section scan the QR code with an app (for example Google Authenticator or Aegis). The admin account controls all servers, so protect it with more than a password.
Verification#
Check the whole installation:
systemctl is-active nginx php8.3-fpm mariadb redis-server pteroq
curl -sI https://panel.example.cz | head -n 1- The first command must print
activefive times. curl -sIdownloads only the response headers; the first line will beHTTP/2 302(redirect to the login) orHTTP/2 200.
The Panel is ready. The next step is adding a node with the Wings service, where the game servers will run: Pterodactyl Wings.
Panel behind Cloudflare#
You can put the Panel behind the Cloudflare proxy (orange cloud). This applies to the Panel only, not to Wings (see Pterodactyl Wings).
A node with the same domain as the Panel
If you entered the Panel's domain (panel.example.cz) as the node's FQDN in the Wings guide, don't put the Panel behind the Cloudflare proxy. Browsers couldn't connect to Wings on port 8080, and the server console and files wouldn't work. Either keep the Panel's domain on DNS only, or give the node its own subdomain (for example node.example.cz) with a gray cloud and its own certificate.
In Cloudflare, set the SSL/TLS mode to Full (strict). Cloudflare then connects to the VPS over HTTPS and verifies your Let's Encrypt certificate.
Switch the
panel.example.czrecord to Proxied.Add Cloudflare's IP ranges to
.envon a single line. Behind the proxy, all requests reach the Panel from Cloudflare's addresses. The Panel then takes the visitor's real IP address from theX-Forwarded-Forheader, but only from addresses it trusts. Without this line, the Panel would only see Cloudflare's addresses for all users, both in activity logs and in the brute-force protection, which counts attempts per IP address. The list below is current as of October 1, 2026; you'll find the latest one at cloudflare.com/ips (opens in a new window):/var/ www/ inipterodactyl/ .env 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,2400:cb00::/32,2606:4700::/32,2803:f800::/32,2405:b500::/32,2405:8100::/32,2a06:98c0::/29,2c0f:f248::/32TRUSTED_PROXIES– a list of addresses and ranges (comma-separated, no spaces) from which the Panel acceptsX-Forwarded-*headers with the visitor's real IP address and protocol. It ignores these headers from any other address, so nobody can spoof them.
Load the new configuration:
cd /var/www/pterodactyl && php artisan config:clear.
Don't use Flexible mode
In Flexible mode, traffic between Cloudflare and the VPS goes unencrypted over HTTP, but Nginx redirects it to HTTPS. The result is an endless redirect loop (ERR_TOO_MANY_REDIRECTS). Use Full (strict).
Updating the Panel#
New Panel versions fix bugs and security holes, so update regularly. Before updating, read the release notes on GitHub and check which PHP version the new release requires (php -v).
Back up first
Before updating, back up the database and the .env file:
mariadb-dump --single-transaction panel > /root/panel-$(date +%F).sql
cp /var/www/pterodactyl/.env /root/pterodactyl-env-$(date +%F).bakmariadb-dump --single-transaction panel– exports thepaneldatabase to an SQL file without locking tables.$(date +%F)– inserts today's date (for example2026-10-01) into the file name.
Then run the update:
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| Command | What it does |
|---|---|
php artisan down | Turns on maintenance mode; users see a maintenance page instead of errors during the update. |
curl -L … | tar -xzv | Downloads the latest release and unpacks it straight over the existing files. .env isn't overwritten; it isn't in the archive. |
chmod -R 755 … | Restores write permissions on the directories. |
composer install … | Installs the libraries for the new version. |
view:clear, config:clear | Clears compiled templates and the configuration cache so the new ones are loaded. |
migrate --seed --force | Updates the tables for the new version and refreshes the default game templates. Your own edits to default templates are overwritten; if you change a template, make a copy of it. |
chown -R www-data:www-data … | Gives ownership of the files back to the web server. |
queue:restart | Tells the pteroq service to load the new code after finishing its current job. |
php artisan up | Turns off maintenance mode. |
After updating the Panel, update Wings too; the steps are in the Pterodactyl Wings guide. Panel and Wings versions should go hand in hand, so use the latest release of both.
Panel backups#
To restore the Panel you need two things: the panel database and the .env file (above all APP_KEY). Game server data lives on the Wings node, not in the Panel, so back it up separately. How to automate backups and get them off the server is covered in Backups on a VPS.
Troubleshooting#
502 Bad Gateway#
Nginx couldn't reach PHP-FPM. Check that PHP-FPM is running and that the socket path matches:
systemctl status php8.3-fpm
ls /run/php/The ls output must include php8.3-fpm.sock, and the same path must be in the fastcgi_pass directive. If the service isn't running, start it: systemctl enable --now php8.3-fpm.
Error 500 or a blank page#
Look at the Panel's log:
tail -n 100 /var/www/pterodactyl/storage/logs/laravel-$(date +%F).logtail -n 100– prints the last 100 lines of today's log.
Look for a line with a date and the word ERROR. The most common causes:
Permission denied– wrong file owner. Runchown -R www-data:www-data /var/www/pterodactyl/*again.No application encryption key has been specified.–APP_KEYis missing. On the first installation, runphp artisan key:generate --force. If the Panel already has data, restore the original key from your backup.SQLSTATE[HY000] [1045] Access denied– the database password or user in.envis wrong.
The MAC is invalid#
The database was encrypted with a different APP_KEY than the one now in .env. This typically happens after restoring the database onto a new installation. The only fix is to put the original APP_KEY back into .env. Without it, the encrypted data can't be recovered.
Emails aren't sent#
Most often the queue isn't running. Check the service and its log:
systemctl status pteroq
journalctl -u pteroq -n 50journalctl -u pteroq -n 50– the last 50 lines of the service log.
Also check that Redis is running (systemctl status redis-server). Once fixed, run systemctl restart pteroq. If the queue is running, check the mail settings (php artisan p:environment:mail) and errors in the Panel log (see Error 500).
A server is stuck installing#
Server installation is done by Wings, not the Panel. See the “Troubleshooting” section of the Pterodactyl Wings guide (most often broken DNS in containers or an unreachable node).
Scheduled server tasks don't run#
Check that the queue is running (systemctl status pteroq), the cron line (crontab -l) and the time zones: the system (timedatectl) and the Panel (APP_TIMEZONE in .env). Clearing the scheduler cache can also help:
php /var/www/pterodactyl/artisan schedule:clear-cache
systemctl restart pteroqNginx won't start: cannot load certificate#
The certificate for the domain in the configuration doesn't exist: you didn't get it, or the domain in server_name and in the /etc/letsencrypt/live/… path differs from the one it was issued for. certbot certificates lists your certificates.
Certbot: Timeout during connect (likely firewall problem)#
Let's Encrypt couldn't reach port 80 of your VPS. Check that the domain's A record points to the VPS's IPv4 address, that port 80 is allowed in both the customer area firewall and UFW, and that Nginx is running. If the domain has an AAAA record, it must point to this VPS's IPv6 address, because Let's Encrypt tries IPv6 too.
Composer or artisan reports the wrong PHP version#
The message says something like requires php ^8.2 || ^8.3 but your php version (8.4…) does not satisfy that requirement. The php command runs a version other than 8.3, typically on Debian 13 after the default php package got installed. Set the default PHP to 8.3:
update-alternatives --set php /usr/bin/php8.3
php -vupdate-alternatives --set php …– sets which version thephpcommand runs.
If /usr/bin/php8.3 doesn't exist, PHP 8.3 isn't installed. Go back to Installing dependencies.
The page loops in redirects behind Cloudflare (ERR_TOO_MANY_REDIRECTS)#
Cloudflare is in Flexible mode. Switch it to Full (strict) and also check TRUSTED_PROXIES as described in Panel behind Cloudflare.
The server console doesn't work behind Cloudflare#
The node's domain goes through the Cloudflare proxy (orange cloud). This typically happens when the node uses the same domain as the Panel. Switch the node's domain to DNS only, or give the node its own subdomain.
Summary
- The Panel runs on Nginx + PHP 8.3 (on Debian 13 from the
packages.sury.orgrepository), MariaDB and Redis. - The Panel's database user has host
127.0.0.1and privileges on thepaneldatabase only. - You have
APP_KEYfrom.envsaved off the server; without it, encrypted data can't be restored. - Cron runs
schedule:runevery minute; thepteroqservice processes the queue. - The certificate renews itself and Nginx loads it after renewal thanks to the deploy hook.
- Next step: Pterodactyl Wings and the database host for servers.
Further reading#
- Pterodactyl: Getting Started (opens in a new window)
- Pterodactyl: Webserver Configuration (opens in a new window)
- Pterodactyl: Updating the Panel (opens in a new window)
- Pterodactyl: Additional Configuration (opens in a new window)
- Pterodactyl: Troubleshooting (opens in a new window)
- Certbot: User Guide (opens in a new window)