Pterodactyl uses Laravel’s migration system to manage database schema changes. This guide covers running, creating, and troubleshooting migrations.
What are Migrations?
Migrations are version control for your database schema. They allow you to:
- Define database structure changes in PHP code
- Roll back changes if needed
- Share schema changes across environments
- Track database version history
Running Migrations
Initial Installation
During first-time setup, run all migrations:
php artisan migrate --seed --force
Flags:
--seed: Run database seeders after migrations
--force: Required in production (bypasses confirmation)
Updating Panel
When updating Pterodactyl, run migrations to apply schema changes:
cd /var/www/pterodactyl
php artisan down
# Update code (git pull, composer install, etc.)
php artisan migrate --force
php artisan view:clear
php artisan config:clear
php artisan up
Auto-Upgrade (v1.3.0+)
Pterodactyl v1.3.0+ includes self-upgrade functionality:
This automatically:
- Backs up the current installation
- Downloads the latest release
- Runs migrations with
--force and --seed
- Clears caches
Migration Status
Check which migrations have been run:
php artisan migrate:status
Output:
+------+----------------------------------------------------+-------+
| Ran? | Migration | Batch |
+------+----------------------------------------------------+-------+
| Yes | 2017_09_10_225941_CreateSchedulesTable | 1 |
| Yes | 2017_09_10_230309_CreateNewTasksTableForSchedules | 1 |
| Yes | 2021_01_17_152623_add_generic_server_status_column| 1 |
| No | 2024_07_13_091852_clear_unused_allocation_notes | |
+------+----------------------------------------------------+-------+
Common Migrations
Server State Management
2021_01_17_152623_add_generic_server_status_column
Added unified status column to replace multiple state columns:
Schema::table('servers', function (Blueprint $table) {
$table->string('status')->default('installing')->after('suspended');
});
Possible statuses:
installing
install_failed
reinstall_failed
suspended
restoring_backup
null (normal operation)
API Key Enhancements
2023_02_23_191004_add_expires_at_column_to_api_keys_table
Added expiration support for API keys:
Schema::table('api_keys', function (Blueprint $table) {
$table->timestamp('expires_at')->nullable()->after('last_used_at');
});
2022_06_18_112822_track_api_key_usage_for_activity_events
Links activity logs to API keys:
Schema::table('activity_logs', function (Blueprint $table) {
$table->unsignedInteger('api_key_id')->nullable()->after('actor_id');
});
Backup Features
2021_05_03_201016_add_support_for_locking_a_backup
Added backup locking to prevent deletion:
Schema::table('backups', function (Blueprint $table) {
$table->boolean('is_locked')->default(false);
});
2020_12_26_184914_add_upload_id_column_to_backups_table
Added support for S3 multipart uploads:
Schema::table('backups', function (Blueprint $table) {
$table->text('upload_id')->nullable()->after('disk');
});
Server Transfers
2020_04_04_131016_add_table_server_transfers
Created table for tracking server transfers:
Schema::create('server_transfers', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('server_id');
$table->boolean('successful')->nullable();
$table->unsignedInteger('old_node');
$table->unsignedInteger('new_node');
$table->unsignedInteger('old_allocation');
$table->unsignedInteger('new_allocation');
$table->json('old_additional_allocations')->nullable();
$table->json('new_additional_allocations')->nullable();
$table->timestamps();
});
Schedule Improvements
2021_01_13_013420_add_cron_month
Added month field to schedule cron:
Schema::table('schedules', function (Blueprint $table) {
$table->string('cron_month', 191)->default('*')->after('cron_day_of_week');
});
2021_05_01_092523_add_only_run_when_server_online_option_to_schedules
Added conditional execution:
Schema::table('schedules', function (Blueprint $table) {
$table->boolean('only_when_online')->default(false);
});
Creating Custom Migrations
Only create custom migrations if you’re developing Panel modifications. Do not modify core migrations.
Generate Migration
php artisan make:migration add_custom_field_to_servers_table
Migration Structure
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->string('custom_field')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('custom_field');
});
}
};
Common Schema Operations
Add Column:
$table->string('column_name')->nullable();
$table->integer('count')->default(0);
$table->text('description')->after('name');
Modify Column:
$table->string('name', 500)->change();
$table->integer('value')->unsigned()->change();
Drop Column:
$table->dropColumn('column_name');
$table->dropColumn(['col1', 'col2']);
Add Index:
$table->index('email');
$table->unique('username');
$table->foreign('user_id')->references('id')->on('users');
Drop Index:
$table->dropIndex('users_email_index');
$table->dropUnique('users_username_unique');
$table->dropForeign('servers_user_id_foreign');
Rollback Migrations
Rolling back migrations can cause data loss. Always backup your database first.
Rollback Last Batch
php artisan migrate:rollback
Rollback Specific Steps
# Rollback last 3 migration batches
php artisan migrate:rollback --step=3
Rollback All
php artisan migrate:reset
Rollback and Re-run
php artisan migrate:refresh
# With seeding
php artisan migrate:refresh --seed
Migration Best Practices
1. Always Backup First
mysqldump -u root -p panel > backup_$(date +%Y%m%d_%H%M%S).sql
2. Test in Development
Never run untested migrations in production:
# In development
php artisan migrate
php artisan migrate:rollback
php artisan migrate
3. Use Transactions
For complex migrations:
DB::transaction(function () {
// Migration operations
});
4. Handle Data Migration
When changing column types, migrate existing data:
public function up(): void
{
// Add new column
Schema::table('servers', function (Blueprint $table) {
$table->string('status_new')->nullable();
});
// Migrate data
DB::table('servers')->where('installing', true)
->update(['status_new' => 'installing']);
// Drop old column
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('installing');
});
// Rename new column
Schema::table('servers', function (Blueprint $table) {
$table->renameColumn('status_new', 'status');
});
}
Troubleshooting
”Nothing to migrate”
All migrations are already run. Check status:
php artisan migrate:status
“Syntax error or access violation”
Database user lacks required permissions:
GRANT ALL PRIVILEGES ON panel.* TO 'pterodactyl'@'localhost';
FLUSH PRIVILEGES;
“SQLSTATE[42S01]: Base table or view already exists”
Migration already partially applied. Options:
- Rollback and retry
- Manually mark as run:
php artisan migrate:rollback --step=1
php artisan migrate
“Class not found” errors
Clear autoload cache:
composer dump-autoload
php artisan config:clear
“Migration table not found”
Database not initialized. Run:
php artisan migrate:install
php artisan migrate
MySQL 8 Compatibility
If encountering errors with MySQL 8:
-- Use native password authentication
ALTER USER 'pterodactyl'@'localhost'
IDENTIFIED WITH mysql_native_password BY 'password';
MariaDB Compatibility
Some migrations may fail on MariaDB < 10.5. Upgrade to 10.5+ or use MySQL 8.
Database Maintenance
Optimize Tables
Repair Tables
REPAIR TABLE servers;
REPAIR TABLE allocations;
Check Table Status
CHECK TABLE servers;
CHECK TABLE api_keys;
Migration History
Key migration milestones:
- v1.11.0: Added activity logging tables
- v1.8.0: Changed egg format to PTDL_v2
- v1.6.0: Added server transfer support
- v1.3.0: Unified server status column
- v1.0.0: Complete database restructure
- v0.7.x: Legacy structure (pre-1.0)
Further Reading