Run game servers with Pelican

Pelican on Debian 13, a free panel that runs Minecraft and other game servers in containers, with its Wings daemon on the same server, Let's Encrypt certificates, IPv4 and IPv6 for players, backups and SFTP.

Tested on Pelican Panel 1.0.0-beta38 and Wings 1.0.0-beta29 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

Pelican is a panel for game servers. From your browser, you create a Minecraft server, or a server for one of hundreds of other games, give it memory and a port, and start it. You and your friends get a console, a file manager, backups and SFTP access, each with their own login.

Pelican has two parts. The Panel is the website, a PHP app. Wings is a daemon that runs each game server in its own Docker container. This guide puts both on the same server, with Caddy or nginx as the web server. Pelican is still in beta, so read the release notes before each update.

Wings runs as root and needs Docker running as root, not rootless Podman like our other guides: it creates a container for every game server, sets its memory and CPU limits, and publishes its game ports, all through Docker's root socket. So give Pelican a server of its own.

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

  • The Panel was installed with PHP 8.4 from Debian, and got a Let's Encrypt certificate from Caddy or from Certbot, which Wings uses too. The Panel answered over IPv4 and IPv6, and sent HTTP to HTTPS.
  • The web installer was reachable only from our own address until it was done, which we checked from outside. Scanners opened the new name within two minutes of its certificate being issued.
  • A Paper Minecraft server was installed and started in about 30 seconds, and answered a Minecraft status ping over IPv4 from outside and over IPv6 from the server itself. The console, the file manager and SFTP worked.
  • A backup of the game server took about a second, and restoring it brought back a deleted world.
  • With Caddy, a new certificate for Wings was picked up within seconds. The Panel's update script, a restart of Wings and a reboot left everything running, including the game server.

With one idle Minecraft server running, the server used about 1.7 GB of memory, of which the game server took about 1 GB.

Before you start

Pick the web server. Caddy gets and renews its certificates by itself. nginx uses Certbot for them. Both follow Pelican's own configuration.

Web server

You need:

  • a fresh Melonslab server with Debian 13 and nothing else installed. The Panel and Wings need about 1 GB of memory, and each game server needs its own on top of that: 2 to 4 GB for a small Minecraft server;

With Caddy

  • two names for the server, each with an A record and an AAAA record pointing at it: one for the Panel, such as pelican.example.com, and one for Wings, such as wings.example.com. Caddy gets a certificate for each, and Wings uses its own. A name of its own for Wings also lets you move Wings to another server later.

With nginx

  • a name for the server, such as pelican.example.com, with an A record and an AAAA record pointing at it. The Panel, Wings and your players all use it.

Set up SSH keys, automatic security updates and the firewall as in steps 1 to 4 of the security guide, including the rules for HTTP and HTTPS. Step 10 here explains which ports ufw controls and which it does not.

The examples use pelican.example.com for the Panel and 198.51.100.20 for the address you browse from. Replace them with your own throughout.

1. Install PHP and the web server

Pelican needs PHP 8.3 or newer. Debian 13 has PHP 8.4. As root:

With Caddy

apt update
apt install -y curl tar unzip caddy \
  php8.4-fpm php8.4-gd php8.4-mysql php8.4-mbstring php8.4-bcmath php8.4-xml \
  php8.4-curl php8.4-zip php8.4-intl php8.4-sqlite3

This is Debian's own Caddy, version 2.6.2. It is older than the version in Caddy's own package repository, but it runs Pelican's Caddy configuration as it is, and its security updates come from Debian with the rest of the system.

With nginx

apt update
apt install -y curl tar unzip nginx python3-certbot-nginx \
  php8.4-fpm php8.4-gd php8.4-mysql php8.4-mbstring php8.4-bcmath php8.4-xml \
  php8.4-curl php8.4-zip php8.4-intl php8.4-sqlite3

Open the ports for Wings too: 8080 is its API, which your browser connects to for the console, and 2022 is its SFTP server:

ufw allow 8080,2022/tcp

2. Download the Panel

mkdir -p /var/www/pelican
cd /var/www/pelican
curl -L https://github.com/pelican-dev/panel/releases/latest/download/panel.tar.gz | tar -xz
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
COMPOSER_ALLOW_SUPERUSER=1 composer install --no-dev --optimize-autoloader
php artisan p:environment:setup

The last command creates /var/www/pelican/.env with an encryption key, APP_KEY, which protects secrets in the database. Keep a copy of it somewhere safe, away from the server, such as in a password manager: without it, a backup of the database cannot be decrypted.

Set the Panel's address before you open it. The installer page loads its styles from this address, and shows up blank without them:

sed -i 's|^APP_URL=.*|APP_URL=https://pelican.example.com|' .env
chmod -R 755 storage/* bootstrap/cache/
chown -R www-data:www-data /var/www/pelican

3. Get a certificate and set up the web server

Until you have finished Pelican's web installer in step 4, anyone who opens it can create the admin account and point the Panel at a database of their own. The two lines marked # installer below let only your own address in until then.

With Caddy

If your connection has IPv6, add your IPv6 address after 198.51.100.20 on the same line, with a space between them.

Replace /etc/caddy/Caddyfile with:

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

pelican.example.com {
    @notyou not remote_ip 198.51.100.20 # installer
    respond @notyou 403 # installer

    root * /var/www/pelican/public

    file_server

    php_fastcgi unix//run/php/php8.4-fpm.sock {
        root /var/www/pelican/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/pelican.log {
            roll_size 100MiB
            roll_keep_for 7d
        }
        level INFO
    }
}

wings.example.com {
    respond 404
}

This is Pelican's own configuration with three changes: it uses PHP 8.4's socket, it has the two # installer lines, and it has a block for wings.example.com, which only makes Caddy get a certificate for Wings. Wings itself listens on port 8080 with that certificate, and nothing is served on the name's port 443. Then load it:

caddy validate --config /etc/caddy/Caddyfile
systemctl restart caddy

Within a minute, Caddy has certificates from Let's Encrypt for both names, and renews them by itself. It listens on IPv4 and IPv6, sends HTTP to HTTPS, and also offers HTTP/3 on UDP port 443, which the security guide's rule allows. The Panel sees each visitor's real address, as the logins under Activity in your profile show.

With nginx

If your connection has IPv6, add a third allow line with your IPv6 address, before the deny line. First, get the certificate:

certbot certonly --nginx -d pelican.example.com

Certbot asks for an email address and for you to accept Let's Encrypt's terms, and saves the certificate under /etc/letsencrypt/live/pelican.example.com/. It renews by itself.

Create /etc/nginx/sites-available/pelican.conf:

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

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

    allow 198.51.100.20; # installer
    deny all; # installer

    root /var/www/pelican/public;
    index index.php;

    access_log /var/log/nginx/pelican.app-access.log;
    error_log  /var/log/nginx/pelican.app-error.log error;

    client_max_body_size 100m;
    client_body_timeout 120s;

    sendfile off;

    ssl_certificate /etc/letsencrypt/live/pelican.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pelican.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;
    }
}

This is Pelican's own configuration with three changes: it listens on IPv6 too, it uses PHP 8.4's socket, and it leaves out server_tokens off, which Debian's nginx.conf already sets. Then switch it on:

rm /etc/nginx/sites-enabled/default
ln -s /etc/nginx/sites-available/pelican.conf /etc/nginx/sites-enabled/pelican.conf
nginx -t
systemctl restart nginx

4. Run the web installer

The Panel needs a scheduled task and a queue worker, which also installs the game templates you pick in the installer. Set both up first:

(crontab -l -u www-data 2>/dev/null; echo "* * * * * php /var/www/pelican/artisan schedule:run >> /dev/null 2>&1") | crontab -u www-data -
cd /var/www/pelican
php artisan p:environment:queue-service -n

The last command creates and starts pelican-queue.service. Now open https://pelican.example.com/installer, and go through the steps:

  • Environment: keep the App URL, and create your admin account under Admin User with an email address, a username and a long password.
  • Database: keep SQLite, which Pelican recommends. The database is the file /var/www/pelican/database/database.sqlite.
  • Eggs: eggs are Pelican's templates for games. Open the Minecraft tab and tick Paper, and anything else you want. You can import more later under Eggs.
  • Cache, Queue and Session: keep the defaults. On Queue, switch on I have done both steps below, as you did them above.

Choose Finish. You are logged in to the admin area, and the installer is gone: /installer now answers 404. Remove the two # installer lines, so that everyone else can reach the Panel too, and make the files with secrets readable only by the web server:

With Caddy

sed -i '/# installer/d' /etc/caddy/Caddyfile
systemctl reload caddy
chmod 600 /var/www/pelican/.env /var/www/pelican/database/database.sqlite

With nginx

sed -i '/# installer/d' /etc/nginx/sites-available/pelican.conf
systemctl reload nginx
chmod 600 /var/www/pelican/.env /var/www/pelican/database/database.sqlite

Both files are readable by every user on the server until you do.

5. Install Docker and Wings

This is Docker's own install script, which Pelican's documentation uses:

curl -sSL https://get.docker.com/ | CHANNEL=stable sh
mkdir -p /etc/pelican /var/run/wings
curl -L -o /usr/local/bin/wings https://github.com/pelican-dev/wings/releases/latest/download/wings_linux_amd64
chmod u+x /usr/local/bin/wings

Create /etc/systemd/system/wings.service:

[Unit]
Description=Wings Daemon
After=docker.service
Requires=docker.service
PartOf=docker.service

[Service]
User=root
WorkingDirectory=/etc/pelican
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

6. Create the node

A node is a server that runs Wings. In the Panel, open Nodes and choose New Node:

With Caddy

  • Domain Name: wings.example.com.

With nginx

  • Domain Name: pelican.example.com. The Panel checks the name and shows Valid DNS with your address.
  • Keep Port 8080 and HTTPS (SSL), and give it a Display Name, such as Stockholm.
  • Go on to Advanced Settings. The defaults are fine: server files in /var/lib/pelican/volumes and SFTP on port 2022. Choose the plus sign to create the node.

The node's Configuration File tab shows the file Wings needs. Write it to the server:

cd /var/www/pelican
php artisan p:node:configuration 1 > /etc/pelican/config.yml
chmod 600 /etc/pelican/config.yml

1 is the node's number, which you also see in the browser's address bar, as in /admin/nodes/1/edit. The file holds the node's secret token, so keep it to root.

With Caddy

The file expects the certificate where Certbot saves it. Point it at the files Caddy keeps for wings.example.com instead. Wings runs as root, so it can read them, although they belong to the caddy user:

C=/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/wings.example.com
sed -i "s|^    cert: .*|    cert: $C/wings.example.com.crt|; s|^    key: .*|    key: $C/wings.example.com.key|" /etc/pelican/config.yml
grep -E '^    (cert|key):' /etc/pelican/config.yml

The last command shows the two new paths. When you save the node in the Panel later, it sends Wings the Certbot paths again, but Wings keeps the ones you set. Start Wings:

systemctl daemon-reload
systemctl enable --now wings

Wings reads the certificate only when it starts, and Caddy renews it about a month before it runs out, without telling Wings. A systemd path unit restarts Wings when that happens. It watches the .json file that Caddy writes last, after the certificate and the key. Create /etc/systemd/system/wings-cert.path:

[Unit]
Description=Watch the Wings certificate that Caddy renews

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

[Install]
WantedBy=multi-user.target

And /etc/systemd/system/wings-cert.service:

[Unit]
Description=Restart Wings to load a renewed certificate

[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl restart wings.service
systemctl daemon-reload
systemctl enable --now wings-cert.path

We tested it by making Caddy get a new certificate: Wings restarted and served it within seconds.

With nginx

The paths to your certificate are already filled in. Start Wings:

systemctl daemon-reload
systemctl enable --now wings

Wings reads the certificate only when it starts, so restart it whenever the certificate is renewed. Create /etc/letsencrypt/renewal-hooks/deploy/pelican:

#!/bin/sh
systemctl reload nginx
systemctl restart wings
chmod +x /etc/letsencrypt/renewal-hooks/deploy/pelican

After a minute, the node's Overview shows the Wings version and the server's memory and disk. Restarting Wings leaves running game servers running.

7. Add a port for players

An allocation is an address and a port that a game server listens on. Wings passes the address to Docker as it is: 0.0.0.0 means every IPv4 address and :: every IPv6 address, but each only one of the two. For players to reach a game over both, give it the same port on both.

Open the node, and under Allocations, choose New allocation:

  • Pick 0.0.0.0 as the IP Address, type 25565, Minecraft's port, under Ports and press Enter, and choose Create.
  • Do the same again with :: as the IP Address.

8. Create a Minecraft server

Open Servers and choose New Server:

  • Information: a Name, such as Survival, and an Owner. Pick 0.0.0.0:25565 as the Primary Allocation and [::]:25565 under Additional Allocations.
  • Egg Configuration: pick Paper. The defaults download the latest version of Minecraft and start the server after installing it.
  • Environment Configuration: under Memory, choose Limited and give it a Memory Limit, such as 4096 MiB. Set Swap Memory to Disabled. Under Backups, allow a number of backups, such as 2: the default, 0, allows none.

Choose the plus sign to create it. Wings downloads the Java image and Paper, which took about 30 seconds on our test server. The first start stops at Minecraft's licence. Open the server from the Panel's front page, choose Start, and I Accept under Minecraft EULA. The console shows the server starting, and Status shows Running.

Players connect to pelican.example.com, or any other name that points at the server. Minecraft uses port 25565 by default, so they do not need to add it.

9. Users and logins

Pelican has no sign-up page: /register answers 404, and only an admin can create accounts, under Users and New User. To share a server with someone, add their account under the server's Users.

Pelican sends no email until you set up a mail server under Settings and Mail: until then, messages such as password resets are only written to the Panel's log. Give each account a password when you create it.

Under your name, Profile has 2FA, Passkeys and SSH Keys. Switch on two-factor authentication for every admin. SFTP asks only for the password, not the second factor, so a long password matters, or an SSH key added there.

For SFTP, the server's Settings show the address and the username under SFTP Information. The username is yours, a dot and the server's short ID:

With Caddy

sftp -P 2022 anna.1a2b3c4d@wings.example.com

With nginx

sftp -P 2022 anna.1a2b3c4d@pelican.example.com

10. What is open to the internet

PortUsed byControlled by
80, 443Panel, and Let's Encrypt renewalsufw
8080Wings API, for the console in the browserufw
2022Wings SFTPufw
25565 and other game portsgame serversDocker

The Panel, Wings and SFTP run directly on the server, so ufw decides who reaches them. Game ports are different. Docker opens a game server's ports when it starts, in its own firewall chain that ufw's rules never see: on our test server, ufw deny 25565/tcp did not stop players from connecting. Docker publishes each game port for both TCP and UDP. Ports that no running server uses stay closed.

So the game ports that are open are those of the servers that are running. To close a game port that a running server uses, add a DROP rule in Docker's DOCKER-USER chain, as in step 5 of the Coolify guide.

11. Back up

Open the game server and Backups, and choose New backup. Wings saves it as a .tar.gz file in /var/lib/pelican/backups. To restore one, open its Actions and choose Restore. The server stops while it runs, and Delete all files before restoring backup? removes files that were added after the backup. On our test server, a 208 MB backup took about a second, and restoring it about two. A server's Schedules can also take backups by themselves, which we did not test.

A backup that stays on the server does not help if the server is lost. Copy /var/lib/pelican/backups elsewhere, or add an S3-compatible bucket under Backup Hosts with Schema set to S3, which we did not test. For the Panel itself, keep a copy of /var/www/pelican/.env and /var/www/pelican/database/database.sqlite.

12. Updates

Pelican's update script backs up the .env file and the SQLite database to /var/www/pelican/backup, replaces the Panel's files and updates the database. Answer its questions with Enter to keep the defaults, then set the permissions it asks for:

bash -c "$(curl -fsSL https://pelican.dev/updatePanel.sh)"
chmod -R 755 /var/www/pelican/storage/* /var/www/pelican/bootstrap/cache
chown -R www-data:www-data /var/www/pelican

Wings updates itself, and running game servers keep running:

wings update
systemctl restart wings

The admin Dashboard shows when a new Panel version is out, and a node's Overview shows its Wings version next to the latest. Pelican checks both with GitHub. We found no telemetry in the Panel or in Wings.

Troubleshooting

The installer page is blank, with no styles. APP_URL in /var/www/pelican/.env is not the address you opened. Set it as in step 2, and reload the page.

With Caddy

The installer answers "403 Forbidden". The address you browse from is not the one on the remote_ip line. If your connection has IPv6, your browser probably uses that. Add it on the same line, and run systemctl reload caddy.

Wings does not start, and journalctl -u wings shows "no such file or directory" for the certificate. Caddy has not got the certificate for wings.example.com yet, or got it from ZeroSSL, which Caddy falls back to when Let's Encrypt fails. ls /var/lib/caddy/.local/share/caddy/certificates/ shows which, and journalctl -u caddy why. Fix the path in /etc/pelican/config.yml and in wings-cert.path if it is under ZeroSSL's directory.

With nginx

The installer answers "403 Forbidden". The address you browse from is not the one in the allow line. If your connection has IPv6, your browser probably uses that. Add it in its own allow line, and reload nginx.

The console stays empty, and Start does nothing. Your browser connects to Wings on port 8080. Check systemctl status wings, and that ufw status allows 8080. Running wings --debug after systemctl stop wings shows why Wings will not start.

Players cannot connect over IPv4, or over IPv6. The server has an allocation for only one of the two. Add the other as in step 7. In the admin area, open the server's Allocations, choose Associate and pick it, then restart the server.

The server stops right after starting, with "You need to agree to the EULA". Choose I Accept in the dialog that the console shows.

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