Docs/Guides/Database Management Guide

Database Management Guide

This guide explains how Shipper manages databases for your applications.

Overview

Shipper provides automatic database lifecycle management:

  • Creating databases with secure passwords
  • Managing database users
  • Linking databases to sites
  • Variable interpolation for dynamic names
  • Automatic cleanup on site destruction

Database Configuration

Basic Configuration

Define databases in your project configuration:

projects:
  api:
    databases:
      main:
        name: "myapp_production"
        user: "myapp_production"
        type: mysql

Multiple Databases

Configure multiple databases per project:

databases:
  main:
    name: "myapp_production"
    user: "myapp_production"
    type: mysql
  cache:
    name: "myapp_cache"
    user: "myapp_cache"
    type: mysql
  analytics:
    name: "myapp_analytics"
    user: "myapp_analytics"
    type: mysql

Database Options

  • name (required): Database name (supports variable interpolation)
  • user (required): Database username (supports variable interpolation)
  • type (required): Database type (mysql currently supported)

Variable Interpolation

Use variables to create dynamic database names for different environments.

Available Variables

Built-in Variables:

  • ${PROJECT_NAME}: Project name from configuration (e.g., api)
  • ${PROFILE}: Deployment profile (e.g., production, staging, preview)

Environment Variables:

  • ${GITHUB_PR_NUMBER}: Pull request number (for PR previews)
  • ${GITHUB_HEAD_REF}: Branch name (for PR previews)
  • Any custom environment variable

Variable Examples

Pattern: myapp_${PROJECT_NAME}_${PROFILE}

Results:

  • Project api, Profile production: myapp_api_production
  • Project frontend, Profile staging: myapp_frontend_staging

Pattern: myapp_${PROJECT_NAME}_${PROFILE}_${GITHUB_PR_NUMBER}

Results:

  • Project api, Profile preview, PR #123: myapp_api_preview_123
  • Project api, Profile preview, PR #456: myapp_api_preview_456

Variable Handling

Empty Variables:

  • Undefined environment variables are treated as empty strings
  • Example: myapp_${UNDEFINED} becomes myapp_

Cleanup Rules:

  • Trailing underscores are removed
  • Multiple consecutive underscores are collapsed to one
  • Example: myapp__test___ becomes myapp_test

Configuration Examples

Production Database:

databases:
  main:
    name: "myapp_${PROJECT_NAME}_${PROFILE}"
    user: "myapp_${PROJECT_NAME}_${PROFILE}"
    type: mysql

Preview Database (PR-specific):

databases:
  main:
    name: "myapp_${PROJECT_NAME}_${PROFILE}_${GITHUB_PR_NUMBER}"
    user: "myapp_${PROJECT_NAME}_${PROFILE}_${GITHUB_PR_NUMBER}"
    type: mysql

Environment-specific:

databases:
  main:
    name: "myapp_${ENV}_${PROJECT_NAME}"
    user: "myapp_${ENV}_${PROJECT_NAME}"
    type: mysql

Database Lifecycle

1. Creation

When running shipper apply, for each configured database:

  1. Check if exists: Query Ploi for database by name
  2. Create database: If doesn't exist, create with:
    • Specified name (with variables resolved)
    • Secure random password (32 characters)
    • Specified user
  3. Link to site: Associate database with the site
  4. Set environment variables: Inject credentials into site environment

2. Updates

For existing databases:

  • Database is found by name
  • No changes are made to existing databases
  • Still linked to site if not already linked

3. Destruction

When running shipper destroy:

  1. Find all databases: Get databases linked to the site
  2. Remove links: Unlink databases from site
  3. Delete databases: Permanently delete each database
  4. Delete users: Remove database users

Warning: This is permanent and cannot be undone!

Password Management

Automatic Password Generation

  • Passwords are automatically generated (32 characters)
  • Includes uppercase, lowercase, numbers, and special characters
  • Stored securely in Ploi
  • Injected into site environment variables

Accessing Passwords

Via Ploi Dashboard:

  1. Navigate to Databases section
  2. Click on database name
  3. View password

Via Environment Variables: Database credentials are automatically set in site environment:

  • DB_HOST: Database host
  • DB_PORT: Database port
  • DB_DATABASE: Database name
  • DB_USERNAME: Database user
  • DB_PASSWORD: Database password

In Laravel .env:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp_production
DB_USERNAME=myapp_production
DB_PASSWORD=auto_generated_password

Manual Password Changes

To change a database password:

  1. Update password in Ploi dashboard
  2. Update site environment variables
  3. Redeploy the site

Database Types

MySQL

Currently, Shipper supports MySQL databases:

databases:
  main:
    type: mysql

Default Configuration:

  • Port: 3306
  • Host: localhost (same server as site)
  • Charset: utf8mb4
  • Collation: utf8mb4_unicode_ci

Future Database Types

Planned support for:

  • PostgreSQL
  • MariaDB
  • SQLite (file-based)

Multi-Database Scenarios

Separate Read/Write Databases

databases:
  write:
    name: "myapp_${PROFILE}_write"
    user: "myapp_${PROFILE}_write"
    type: mysql
  read:
    name: "myapp_${PROFILE}_read"
    user: "myapp_${PROFILE}_read"
    type: mysql

In Laravel config/database.php:

'mysql' => [
    'write' => [
        'host' => env('DB_WRITE_HOST'),
        'database' => env('DB_WRITE_DATABASE'),
        'username' => env('DB_WRITE_USERNAME'),
        'password' => env('DB_WRITE_PASSWORD'),
    ],
    'read' => [
        'host' => env('DB_READ_HOST'),
        'database' => env('DB_READ_DATABASE'),
        'username' => env('DB_READ_USERNAME'),
        'password' => env('DB_READ_PASSWORD'),
    ],
],

Microservices Architecture

projects:
  users-service:
    databases:
      users:
        name: "users_${PROFILE}"
        user: "users_${PROFILE}"
        type: mysql
  
  orders-service:
    databases:
      orders:
        name: "orders_${PROFILE}"
        user: "orders_${PROFILE}"
        type: mysql

Cache Database

databases:
  main:
    name: "myapp_${PROFILE}"
    user: "myapp_${PROFILE}"
    type: mysql
  cache:
    name: "myapp_cache_${PROFILE}"
    user: "myapp_cache_${PROFILE}"
    type: mysql

Database Migrations

Running Migrations

Add to your deployment script (.ploi/deploy.sh):

#!/bin/bash
set -e


php artisan migrate --force

# Seed data (only for non-production)
if [ "$PLOI_DOMAIN" != "api.example.com" ]; then
  php artisan db:seed --force
fi

Migration Best Practices

  1. Always use --force: Required for production environments
  2. Test locally first: Run migrations locally before deploying
  3. Use transactions: Wrap migrations in database transactions
  4. Backup first: Backup production databases before major migrations
  5. Rollback plan: Have a rollback strategy ready

Zero-Downtime Migrations

For production deployments:

  1. Additive changes first: Add new columns/tables
  2. Deploy code: Deploy new code that works with both old and new schema
  3. Remove old columns: In a later deployment, remove old columns

Example:

Step 1 - Add new column:

Schema::table('users', function (Blueprint $table) {
    $table->string('email_verified')->nullable();
});

Step 2 - Deploy code that uses new column

Step 3 - Remove old column (later):

Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('old_email_field');
});

Database Backups

Manual Backups

Via Ploi Dashboard:

  1. Navigate to Databases
  2. Select database
  3. Click "Backup"

Via CLI (on server):

mysqldump -u username -p database_name > backup.sql

Automated Backups

Configure in Ploi:

  1. Navigate to Server → Backups
  2. Enable automated backups
  3. Set schedule (daily/weekly)
  4. Configure retention period

Custom Backup Script:

#!/bin/bash
# backup-db.sh

DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="myapp_production"
BACKUP_DIR="/backups"

mysqldump -u root -p${DB_PASSWORD} ${DB_NAME} | gzip > ${BACKUP_DIR}/${DB_NAME}_${DATE}.sql.gz

# Remove backups older than 30 days
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +30 -delete

Restoring Backups

From SQL file:

mysql -u username -p database_name < backup.sql

From compressed backup:

gunzip < backup.sql.gz | mysql -u username -p database_name

Preview Database Management

Automatic Preview Databases

For PR previews, use dynamic naming:

databases:
  main:
    name: "myapp_${PROJECT_NAME}_preview_${GITHUB_PR_NUMBER}"
    user: "myapp_${PROJECT_NAME}_preview_${GITHUB_PR_NUMBER}"
    type: mysql

Benefits:

  • Each PR gets its own database
  • No conflicts between PRs
  • Automatic cleanup on PR close

Seeding Preview Data

Add test data seeding to deployment script:

#!/bin/bash
# .ploi/deploy.sh

# Run migrations
php artisan migrate --force

# Seed test data for previews
if [[ "$PLOI_DOMAIN" == *"preview"* ]]; then
  php artisan db:seed --force
fi

Preview Database Cleanup

When PR is closed, the cleanup workflow runs:

./shipper destroy api --profile=preview --force

This automatically:

  1. Deletes the preview site
  2. Deletes the preview database
  3. Removes database user

Troubleshooting

Database Creation Fails

Error: "Database name already exists"

  • Solution: Database name must be unique. Use different pattern or variables.

Error: "Invalid database name"

  • Solution: Database names can only contain letters, numbers, and underscores.

Error: "Permission denied"

  • Solution: Verify Ploi API key has database management permissions.

Connection Failures

Error: "Access denied for user"

  • Solution: Verify database credentials in environment variables.

Error: "Unknown database"

  • Solution: Database wasn't created. Check Shipper logs and Ploi dashboard.

Error: "Too many connections"

  • Solution: Increase max connections in MySQL config or optimize connection pooling.

Migration Failures

Error: "Syntax error in migration"

  • Solution: Test migration locally before deploying.

Error: "Table already exists"

  • Solution: Check migration status with php artisan migrate:status.

Error: "Migration timeout"

  • Solution: Break large migrations into smaller chunks.

Best Practices

  1. Use Variable Interpolation: Always use variables for environment-specific names
  2. Consistent Naming: Use consistent patterns across all databases
  3. Backup Regularly: Enable automated backups for production
  4. Test Migrations: Test migrations on staging before production
  5. Monitor Database Size: Track database growth and optimize queries
  6. Use Transactions: Wrap data modifications in transactions
  7. Clean Up Previews: Always clean up preview databases
  8. Document Schema: Keep database schema documented
  9. Index Properly: Add database indexes for performance
  10. Secure Credentials: Never commit database credentials to code

Database Monitoring

Performance Monitoring

Key Metrics:

  • Query execution time
  • Slow query log
  • Connection pool usage
  • Database size growth

Tools:

  • MySQL Performance Schema
  • Slow query log analysis
  • Ploi monitoring dashboard
  • Third-party monitoring (DataDog, New Relic)

Query Optimization

Laravel Query Debugging:

// Enable query log
DB::enableQueryLog();

// Your queries
User::where('active', true)->get();

// Dump queries
dd(DB::getQueryLog());

Identify Slow Queries:

# Enable slow query log in MySQL
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;

# View slow queries
tail -f /var/log/mysql/slow-query.log

Advanced Topics

Database Replication

For high-availability setups:

  1. Configure master-slave replication
  2. Update connection config in Laravel
  3. Use read replicas for queries
  4. Write to master only

Connection Pooling

Optimize database connections:

// config/database.php
'options' => [
    PDO::ATTR_PERSISTENT => true,
],

Database Sharding

For large-scale applications:

  1. Partition data across multiple databases
  2. Use consistent hashing for shard selection
  3. Implement in application layer

Next Steps