-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall-pterodactyl.sh
More file actions
executable file
·566 lines (501 loc) · 22.3 KB
/
Copy pathinstall-pterodactyl.sh
File metadata and controls
executable file
·566 lines (501 loc) · 22.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
#!/usr/bin/env bash
# Standalone, interactive Pterodactyl Panel bootstrapper.
# Supports Debian, Ubuntu, RHEL, Rocky Linux, and AlmaLinux.
# It does nothing until the operator selects a Panel directory and confirms.
set -Eeuo pipefail
IFS=$'\n\t'
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
ask() { local label="$1" default="$2" value; read -r -p "$label [$default]: " value; printf '%s' "${value:-$default}"; }
required() { local label="$1" value; while :; do read -r -p "$label: " value; [[ -n "$value" ]] && { printf '%s' "$value"; return; }; done; }
secret() { local label="$1" value; read -r -s -p "$label: " value; printf '\n' >&2; printf '%s' "$value"; }
random_secret() { openssl rand -base64 64 | tr -dc 'A-Za-z0-9' | cut -c1-48; }
valid_identifier() { [[ "$1" =~ ^[A-Za-z0-9_]+$ ]]; }
valid_path() { [[ "$1" =~ ^/[A-Za-z0-9_./-]+$ && "$1" != *..* ]]; }
valid_port() { [[ "$1" =~ ^[0-9]+$ && "$1" -ge 1 && "$1" -le 65535 ]]; }
valid_positive_int() { [[ "$1" =~ ^[0-9]+$ && "$1" -gt 0 ]]; }
valid_host() {
[[ "$1" =~ ^[A-Za-z0-9.-]+(:[0-9]+)?$ || "$1" =~ ^[0-9A-Fa-f:]+$ ]]
}
detect_os() {
if [[ -f /etc/os-release ]]; then
. /etc/os-release
OS_ID="$ID"
OS_VERSION="$VERSION_ID"
OS_FAMILY="$ID_LIKE"
else
die "Cannot detect operating system."
fi
}
is_debian_family() { [[ "$OS_FAMILY" == *debian* || "$OS_ID" == debian || "$OS_ID" == ubuntu ]]; }
is_rhel_family() { [[ "$OS_FAMILY" == *rhel* || "$OS_ID" == rocky || "$OS_ID" == almalinux || "$OS_ID" == rhel ]]; }
pkg_install() {
if is_debian_family; then
apt-get install -y "$@"
elif is_rhel_family; then
dnf install -y "$@"
else
die "Unsupported operating system: $OS_ID"
fi
}
pkg_purge() {
if is_debian_family; then
apt-get purge -y "$@"
elif is_rhel_family; then
dnf remove -y "$@"
fi
}
svc_enable() { systemctl enable --now "$1" || die "Could not enable or start service: $1"; }
svc_disable() { systemctl disable --now "$1" || die "Could not disable service: $1"; }
[[ $EUID -eq 0 ]] || die 'Run this script as root.'
command -v openssl >/dev/null 2>&1 || die 'openssl is required.'
command -v systemctl >/dev/null 2>&1 || die 'systemd is required.'
detect_os
is_debian_family || is_rhel_family || die "Unsupported operating system: $OS_ID"
if is_debian_family; then
command -v apt-get >/dev/null 2>&1 || die 'apt-get is required on Debian-family systems.'
else
command -v dnf >/dev/null 2>&1 || die 'dnf is required on RHEL-family systems.'
fi
printf '%s\n' 'Pterodactyl Panel interactive installer'
printf 'Detected OS: %s %s\n' "$OS_ID" "$OS_VERSION"
TARGET="$(ask 'Panel directory' '/var/www/pterodactyl')"
valid_path "$TARGET" || die 'Panel directory must be an absolute path containing only letters, numbers, _, ., /, and - (without ..).'
[[ -f "$TARGET/artisan" ]] || die "No Laravel artisan file found at $TARGET/artisan."
TARGET="$(cd "$TARGET" && pwd)"
printf 'Target: %s\n' "$TARGET"
read -r -p 'Continue? This will install packages and modify the selected target. [y/N]: ' answer
[[ "$answer" =~ ^[Yy]$ ]] || exit 0
if command -v nginx >/dev/null 2>&1; then
read -r -p 'Nginx is installed. Disable and remove it for Caddy? [y/N]: ' REMOVE_NGINX
[[ "$REMOVE_NGINX" =~ ^[Yy]$ ]] || die 'Caddy cannot safely take over while Nginx is retained.'
svc_disable nginx
if is_debian_family; then
pkg_purge nginx nginx-core nginx-common
elif is_rhel_family; then
pkg_purge nginx nginx-common
fi
fi
if command -v httpd >/dev/null 2>&1; then
read -r -p 'Apache (httpd) is installed. Disable and remove it for Caddy? [y/N]: ' REMOVE_APACHE
[[ "$REMOVE_APACHE" =~ ^[Yy]$ ]] || die 'Caddy cannot safely take over while Apache is retained.'
svc_disable httpd
pkg_purge httpd httpd-tools
fi
ENV_FILE="$TARGET/.env"
[[ ! -e "$ENV_FILE" ]] || { cp -a "$ENV_FILE" "$ENV_FILE.before-install.$(date +%Y%m%d%H%M%S)"; }
export DEBIAN_FRONTEND=noninteractive
if is_debian_family; then
apt-get update
apt-get install -y ca-certificates curl git unzip openssl acl gnupg debian-keyring debian-archive-keyring apt-transport-https
apt-get install -y redis-server mariadb-server mariadb-client
apt-get install -y php-cli php-fpm php-mysql php-gd php-mbstring php-bcmath php-xml php-curl php-zip php-intl php-redis
elif is_rhel_family; then
dnf install -y epel-release
dnf install -y ca-certificates curl git unzip openssl acl gnupg2
dnf install -y redis mariadb-server mariadb
dnf install -y php-cli php-fpm php-mysqlnd php-gd php-mbstring php-bcmath php-xml php-curl php-zip php-intl php-redis
dnf install -y mod_ssl
fi
PHP_VERSION_ID="$(php -r 'printf("%d", PHP_VERSION_ID);')"
(( PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80400 )) || die 'Pterodactyl requires PHP 8.2 or 8.3; the installed PHP version is unsupported.'
PANEL_USER="$(stat -c '%U' "$TARGET")"
if [[ "$PANEL_USER" == root || -z "$PANEL_USER" ]]; then
PANEL_USER=www-data
is_rhel_family && PANEL_USER=apache
fi
id "$PANEL_USER" >/dev/null 2>&1 || die "Panel user $PANEL_USER does not exist after package installation."
PANEL_GROUP="$(id -gn "$PANEL_USER")"
if ! command -v caddy >/dev/null 2>&1; then
if [[ ! -f /usr/share/keyrings/caddy-stable-archive-keyring.gpg ]]; then
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
fi
if is_debian_family; then
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' >/etc/apt/sources.list.d/caddy-stable.list
apt-get update
elif is_rhel_family; then
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/rpm.repo' >/etc/yum.repos.d/caddy-stable.repo
fi
pkg_install caddy
fi
if ! command -v composer >/dev/null 2>&1; then
curl -fsSL https://getcomposer.org/installer -o /tmp/composer-setup.php
php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer
rm -f /tmp/composer-setup.php
fi
run_panel() { runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan "$@"; }
APP_URL="$(required 'Panel URL, for example https://panel.example.com')"
[[ "$APP_URL" =~ ^https?://[^/]+/?$ ]] || die 'Panel URL must be an http(s) URL without a path.'
APP_URL="${APP_URL%/}"
APP_HOST="${APP_URL#*://}"
valid_host "$APP_HOST" || die 'Panel URL host contains unsupported characters.'
TIMEZONE="$(ask 'Timezone' 'UTC')"
ADMIN_EMAIL="$(required 'Administrator email')"
ADMIN_USERNAME="$(ask 'Administrator username' 'admin')"
ADMIN_FIRST="$(ask 'Administrator first name' 'Admin')"
ADMIN_LAST="$(ask 'Administrator last name' 'User')"
ADMIN_PASSWORD="$(random_secret)"
DB_MODE="$(ask 'Database mode: local or remote' 'local')"
[[ "$DB_MODE" == local || "$DB_MODE" == remote ]] || die 'Database mode must be local or remote.'
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME="$(ask 'Database name' 'panel')"
DB_USER="$(ask 'Database username' 'pterodactyl')"
valid_identifier "$DB_NAME" || die 'Database name may only contain letters, numbers, and underscores.'
valid_identifier "$DB_USER" || die 'Database username may only contain letters, numbers, and underscores.'
DB_PASSWORD="$(random_secret)"
if [[ "$DB_MODE" == remote ]]; then
DB_HOST="$(required 'Remote database host')"
DB_PORT="$(ask 'Remote database port' '3306')"
valid_host "$DB_HOST" || die 'Remote database host contains unsupported characters.'
valid_port "$DB_PORT" || die 'Remote database port must be between 1 and 65535.'
DB_PASSWORD="$(secret 'Existing remote database password')"
else
svc_enable mariadb
DB_ROOT_PASSWORD="$(secret 'MariaDB root password, blank for socket authentication')"
if [[ -n "$DB_ROOT_PASSWORD" ]]; then
MYSQL_PWD="$DB_ROOT_PASSWORD" mysql --protocol=tcp -h 127.0.0.1 -P 3306 -u root <<SQL
CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '$DB_USER'@'127.0.0.1' IDENTIFIED BY '$DB_PASSWORD';
ALTER USER '$DB_USER'@'127.0.0.1' IDENTIFIED BY '$DB_PASSWORD';
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'127.0.0.1';
FLUSH PRIVILEGES;
SQL
else
mysql --protocol=socket -u root <<SQL
CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '$DB_USER'@'127.0.0.1' IDENTIFIED BY '$DB_PASSWORD';
ALTER USER '$DB_USER'@'127.0.0.1' IDENTIFIED BY '$DB_PASSWORD';
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'127.0.0.1';
FLUSH PRIVILEGES;
SQL
fi
fi
LOCATION_SHORT="$(ask 'First location shortcode' 'home')"
LOCATION_LONG="$(ask 'First location description' 'Primary location')"
NODE_NAME="$(ask 'First node name' 'node-1')"
NODE_DESCRIPTION="$(ask 'First node description' 'Primary node')"
NODE_FQDN="$(required 'Node FQDN or IP')"
valid_host "$NODE_FQDN" || die 'Node FQDN/IP contains unsupported characters.'
NODE_CADDY_HOST="$NODE_FQDN"
[[ "$NODE_FQDN" == *:* ]] && NODE_CADDY_HOST="[$NODE_FQDN]"
NODE_SCHEME="$(ask 'Wings scheme: https or http' 'https')"
[[ "$NODE_SCHEME" == https || "$NODE_SCHEME" == http ]] || die 'Node scheme must be https or http.'
NODE_PROXY="$(ask 'Wings behind reverse proxy? 1 or 0' '0')"
[[ "$NODE_PROXY" == 0 || "$NODE_PROXY" == 1 ]] || die 'Wings proxy setting must be 0 or 1.'
NODE_MEMORY="$(ask 'Node memory in MB' '8192')"
NODE_DISK="$(ask 'Node disk in MB' '51200')"
valid_positive_int "$NODE_MEMORY" || die 'Node memory must be a positive integer.'
valid_positive_int "$NODE_DISK" || die 'Node disk must be a positive integer.'
NODE_API_PORT="$(ask 'Wings API port' '8080')"
NODE_SFTP_PORT="$(ask 'Wings SFTP port' '2022')"
valid_port "$NODE_API_PORT" || die 'Wings API port must be between 1 and 65535.'
valid_port "$NODE_SFTP_PORT" || die 'Wings SFTP port must be between 1 and 65535.'
NODE_BASE="$(ask 'Wings data directory' '/var/lib/pterodactyl/volumes')"
valid_path "$NODE_BASE" || die 'Wings data directory must be an absolute path containing no .. components.'
INSTALL_WINGS="$(ask 'Install Wings and Docker on this host? 1 or 0' '1')"
[[ "$INSTALL_WINGS" == 0 || "$INSTALL_WINGS" == 1 ]] || die 'Install Wings must be 0 or 1.'
CREDENTIALS_FILE="$(cd "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/pterodactyl-credentials.txt"
printf '\nGenerated administrator password: %s\n' "$ADMIN_PASSWORD"
printf 'Generated database password: %s\n' "$DB_PASSWORD"
printf 'Credentials will be saved to: %s\n' "$CREDENTIALS_FILE"
read -r -p 'Continue with these values? [y/N]: ' answer
[[ "$answer" =~ ^[Yy]$ ]] || exit 0
cp "$TARGET/.env.example" "$ENV_FILE"
chmod 640 "$ENV_FILE"
chown "$PANEL_USER":"$PANEL_GROUP" "$ENV_FILE"
run_panel key:generate --force
run_panel p:environment:setup --url="$APP_URL" --timezone="$TIMEZONE" --cache=redis --session=file --queue=redis --no-interaction
run_panel p:environment:database --host="$DB_HOST" --port="$DB_PORT" --database="$DB_NAME" --username="$DB_USER" --password="$DB_PASSWORD" --no-interaction
runuser -u "$PANEL_USER" -- composer install --no-dev --optimize-autoloader --working-dir="$TARGET"
run_panel migrate --seed --force
printf '%s\n' "$ADMIN_PASSWORD" | run_panel p:user:make --email="$ADMIN_EMAIL" --username="$ADMIN_USERNAME" --name-first="$ADMIN_FIRST" --name-last="$ADMIN_LAST" --admin=1
LOCATION_OUTPUT="$(run_panel p:location:make --short="$LOCATION_SHORT" --long="$LOCATION_LONG")"
LOCATION_ID="$(printf '%s\n' "$LOCATION_OUTPUT" | sed -nE 's/.*ID[^0-9]*([0-9]+).*/\1/p' | tail -n1)"
[[ "$LOCATION_ID" =~ ^[0-9]+$ ]] || die 'Could not read the location ID.'
NODE_OUTPUT="$(run_panel p:node:make --name="$NODE_NAME" --description="$NODE_DESCRIPTION" --locationId="$LOCATION_ID" --fqdn="$NODE_FQDN" --public=1 --scheme="$NODE_SCHEME" --proxy="$NODE_PROXY" --maintenance=0 --maxMemory="$NODE_MEMORY" --overallocateMemory=0 --maxDisk="$NODE_DISK" --overallocateDisk=0 --uploadSize=100 --daemonListeningPort="$NODE_API_PORT" --daemonSFTPPort="$NODE_SFTP_PORT" --daemonBase="$NODE_BASE")"
printf '%s\n' "$NODE_OUTPUT"
NODE_ID="$(printf '%s\n' "$NODE_OUTPUT" | sed -nE 's/.*id of ([0-9]+).*/\1/p' | tail -n1)"
[[ "$NODE_ID" =~ ^[0-9]+$ ]] || die 'Could not read the node ID.'
chown -R "$PANEL_USER":"$PANEL_GROUP" "$TARGET/storage" "$TARGET/bootstrap/cache"
run_panel config:cache
run_panel route:cache
run_panel view:cache
if is_debian_family; then
svc_enable redis-server
REDIS_SERVICE=redis-server
elif is_rhel_family; then
svc_enable redis
REDIS_SERVICE=redis
fi
PHP_VERSION="$(php -r 'printf("%d.%d", PHP_MAJOR_VERSION, PHP_MINOR_VERSION);')"
if is_debian_family; then
PHP_FPM_SERVICE="php${PHP_VERSION}-fpm"
PHP_FPM_SOCKET="/run/php/php${PHP_VERSION}-fpm.sock"
else
PHP_FPM_SERVICE='php-fpm'
PHP_FPM_SOCKET='/run/php-fpm/www.sock'
fi
svc_enable "$PHP_FPM_SERVICE"
NODE_CADDY_BLOCK=''
if [[ "$INSTALL_WINGS" == 1 ]]; then
NODE_CADDY_BLOCK="$(cat <<NODE_BLOCK
$NODE_CADDY_HOST {
reverse_proxy 127.0.0.1:$NODE_API_PORT {
transport http {
read_timeout 300s
write_timeout 300s
}
}
header Strict-Transport-Security "max-age=16768000"
header X-Content-Type-Options "nosniff"
header X-XSS-Protection "1; mode=block;"
header X-Robots-Tag "none"
}
NODE_BLOCK
)"
fi
install -d -m 750 /etc/caddy
if [[ -f /etc/caddy/Caddyfile ]]; then
cp -a /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-pterodactyl.$(date +%Y%m%d%H%M%S)"
fi
TEMP_CADDYFILE="$(mktemp /etc/caddy/Caddyfile.XXXXXX)"
trap 'rm -f "$TEMP_CADDYFILE"' EXIT
cat >"$TEMP_CADDYFILE" <<CADDY
{
servers :443 {
timeouts {
read_body 120s
}
}
}
$APP_HOST {
root * "$TARGET/public"
encode zstd gzip
file_server
php_fastcgi unix/$PHP_FPM_SOCKET {
root "$TARGET/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"
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 100MB
}
respond /.ht* 403
log {
output file /var/log/caddy/pterodactyl.log
}
}
$NODE_CADDY_BLOCK
CADDY
if ! caddy validate --config "$TEMP_CADDYFILE"; then
die 'Generated Caddy configuration did not validate; the existing configuration was preserved.'
fi
install -o root -g root -m 640 "$TEMP_CADDYFILE" /etc/caddy/Caddyfile
rm -f "$TEMP_CADDYFILE"
trap - EXIT
svc_enable caddy
install -d -o "$PANEL_USER" -g "$PANEL_GROUP" -m 750 "$TARGET/storage/app/private"
run_panel p:node:configuration "$NODE_ID" --format=yaml >"$TARGET/storage/app/private/node-$NODE_ID.yml"
chmod 640 "$TARGET/storage/app/private/node-$NODE_ID.yml"
if [[ "$INSTALL_WINGS" == 1 ]]; then
printf '\nChecking virtualization and Docker support...\n'
VIRT_TYPE="$(systemd-detect-virt 2>/dev/null || echo 'unknown')"
case "$VIRT_TYPE" in
openvz|lxc|vz) die "Incompatible virtualization detected: $VIRT_TYPE. Wings requires KVM or bare metal." ;;
esac
if ! command -v docker >/dev/null 2>&1; then
if is_debian_family; then
pkg_install docker.io
elif is_rhel_family; then
dnf install -y docker
fi
fi
if ! systemctl is-active --quiet docker 2>/dev/null; then
svc_enable docker
fi
if ! docker info >/dev/null 2>&1; then
die "Docker is installed but not running or not functional. Check Docker configuration."
fi
printf 'Virtualization: %s — Docker: OK\n' "$VIRT_TYPE"
install -d -m 755 /etc/pterodactyl
case "$(uname -m)" in
x86_64) WINGS_ARCH=amd64 ;;
aarch64|arm64) WINGS_ARCH=arm64 ;;
*) die "Unsupported Wings architecture: $(uname -m)" ;;
esac
curl -fL "https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_$WINGS_ARCH" -o /usr/local/bin/wings
chmod 755 /usr/local/bin/wings
cp "$TARGET/storage/app/private/node-$NODE_ID.yml" /etc/pterodactyl/config.yml
chmod 600 /etc/pterodactyl/config.yml
install -d -m 755 /var/run/wings
cat >/etc/systemd/system/wings.service <<'UNIT'
[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
UNIT
systemctl daemon-reload
svc_enable wings
fi
cat >/etc/systemd/system/pterodactyl-queue.service <<UNIT
[Unit]
Description=Pterodactyl Panel Queue Worker
After=$REDIS_SERVICE.service
[Service]
User=$PANEL_USER
Group=$PANEL_GROUP
WorkingDirectory=$TARGET
ExecStart=/usr/bin/php $TARGET/artisan queue:work redis --queue=standard --sleep=3 --tries=3 --max-time=3600
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
UNIT
cat >/etc/cron.d/pterodactyl-panel <<CRON
* * * * * $PANEL_USER cd $TARGET && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
CRON
chmod 644 /etc/cron.d/pterodactyl-panel
systemctl daemon-reload
svc_enable pterodactyl-queue
printf '\nInstallation complete.\nPanel: %s\nAdmin: %s\nNode ID: %s\nWings config: %s/storage/app/private/node-%s.yml\n' "$APP_URL" "$ADMIN_EMAIL" "$NODE_ID" "$TARGET" "$NODE_ID"
if [[ "$INSTALL_WINGS" == 1 ]]; then
printf 'Wings was installed and enabled on this host. The config is at /etc/pterodactyl/config.yml.\n'
else
printf 'Copy the Wings config to /etc/pterodactyl/config.yml on the Wings host, then install and start Wings.\n'
fi
cat >"$CREDENTIALS_FILE" <<CRED
Pterodactyl Panel Credentials
==============================
Panel URL: $APP_URL
Admin Email: $ADMIN_EMAIL
Admin Username: $ADMIN_USERNAME
Admin Password: $ADMIN_PASSWORD
Database Host: $DB_HOST:$DB_PORT
Database Name: $DB_NAME
Database User: $DB_USER
Database Pass: $DB_PASSWORD
Node FQDN: $NODE_FQDN
Node ID: $NODE_ID
Wings Config: $TARGET/storage/app/private/node-$NODE_ID.yml
CRED
chmod 600 "$CREDENTIALS_FILE"
printf '\nCredentials saved to: %s\n' "$CREDENTIALS_FILE"
# --- Auto-updater ---
printf '\n--- Setting up auto-updater ---\n'
install -d -o root -g root -m 755 /etc/pterodactyl
printf '%s\n' "$TARGET" >/etc/pterodactyl/panel-path
chmod 644 /etc/pterodactyl/panel-path
# Seed the marker when the release embeds a version. Without a trustworthy
# marker the updater deliberately skips rather than replacing an unknown panel.
PANEL_VERSION="$(sed -nE "s/^[[:space:]]*'version'[[:space:]]*=>[[:space:]]*'([^']+)'.*/v\1/p" "$TARGET/config/app.php" 2>/dev/null | head -n1 || true)"
if [[ "$PANEL_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
printf '%s\n' "$PANEL_VERSION" >"$TARGET/.panel-version"
chown "$PANEL_USER":"$PANEL_GROUP" "$TARGET/.panel-version"
chmod 640 "$TARGET/.panel-version"
fi
cat >"/etc/cron.daily/pterodactyl-update" <<'UPDATE'
#!/bin/bash
set -Eeuo pipefail
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TARGET_FILE=/etc/pterodactyl/panel-path
[[ -r "$TARGET_FILE" ]] || exit 0
TARGET="$(<"$TARGET_FILE")"
[[ "$TARGET" =~ ^/[A-Za-z0-9_./-]+$ && "$TARGET" != *..* ]] || exit 1
if [[ ! -f "$TARGET/artisan" ]]; then
exit 0
fi
cd "$TARGET"
exec 9>/run/lock/pterodactyl-update.lock
flock -n 9 || exit 0
MAINTENANCE=0
cleanup() {
if [[ "$MAINTENANCE" == 1 ]]; then
runuser -u "$(stat -c '%U' "$TARGET")" -- env HOME="$TARGET" php artisan up || true
fi
[[ -z "${ARCHIVE:-}" ]] || rm -f "$ARCHIVE"
[[ -z "${TAR_LIST:-}" ]] || rm -f "$TAR_LIST"
[[ -z "${STAGING:-}" ]] || rm -rf "$STAGING"
}
trap cleanup EXIT
echo "=== Pterodactyl Panel Auto-Update ==="
echo "Checking for new releases..."
LATEST_TAG=$(curl -fsSL --max-time 30 "https://api.github.com/repos/pterodactyl/panel/releases/latest" | sed -nE 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -n1)
if [[ ! "$LATEST_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9.-]+)?$ ]]; then
echo "Failed to fetch latest release tag."
exit 1
fi
CURRENT_TAG=$(<"$TARGET/.panel-version") || {
echo "No trusted panel version marker; refusing an automatic replacement."
exit 0
}
if [[ "$LATEST_TAG" == "$CURRENT_TAG" ]]; then
echo "Already up to date: $CURRENT_TAG"
exit 0
fi
echo "Updating from $CURRENT_TAG to $LATEST_TAG..."
ARCHIVE=$(mktemp /tmp/pterodactyl-update.XXXXXX.tar.gz)
STAGING=$(mktemp -d /tmp/pterodactyl-update.XXXXXX)
curl -fsSL --max-time 300 "https://github.com/pterodactyl/panel/releases/latest/download/panel.tar.gz" -o "$ARCHIVE"
# Extract outside the live tree. Keep .env and storage untouched by excluding
# them from the staged release, and reject traversal entries before extraction.
TAR_LIST=$(mktemp)
tar -tzf "$ARCHIVE" >"$TAR_LIST"
while IFS= read -r entry; do
[[ "$entry" != /* && "$entry" != ../* && "$entry" != */../* ]] || exit 1
done <"$TAR_LIST"
tar -xzf "$ARCHIVE" -C "$STAGING" --no-same-owner --no-overwrite-dir
[[ -f "$STAGING/artisan" ]] || { echo "Release archive has an unexpected layout." >&2; exit 1; }
rm -rf "$STAGING/.env" "$STAGING/storage"
cp -a "$STAGING/." "$TARGET/"
# Reinstall composer dependencies
PANEL_USER="$(stat -c '%U' "$TARGET")"
[[ "$PANEL_USER" != root && -n "$PANEL_USER" ]] || PANEL_USER=www-data
PANEL_GROUP="$(id -gn "$PANEL_USER")"
chown -R "$PANEL_USER":"$PANEL_GROUP" "$TARGET"
runuser -u "$PANEL_USER" -- env HOME="$TARGET" composer install --no-dev --optimize-autoloader
# Run migrations
runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan down --render="errors::503" || true
MAINTENANCE=1
runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan migrate --seed --force
# Clear caches
runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan config:clear
runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan route:clear
runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan view:clear
runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan cache:clear
# Restart queue worker and leave maintenance mode only after the update steps succeed.
runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan queue:restart
runuser -u "$PANEL_USER" -- env HOME="$TARGET" php artisan up
MAINTENANCE=0
systemctl restart pterodactyl-queue
# Save version
echo "$LATEST_TAG" > "$TARGET/.panel-version"
echo "Update to $LATEST_TAG complete."
UPDATE
chmod 755 /etc/cron.daily/pterodactyl-update
printf 'Auto-updater configured (daily cron).\n'