Multi-Environment WordPress Management with WP-CLI (Dev/Staging/Prod)

Professional WordPress development requires managing multiple environments to test changes safely before deploying to production. WP-CLI streamlines environment management, enabling efficient synchronization, configuration control, and deployment workflows that minimize downtime and reduce errors.

Understanding WordPress Environment Architecture

A robust WordPress workflow typically includes three distinct environments: development for building new features, staging for testing changes in production-like conditions, and production where live users interact with your site.

Each environment serves a specific purpose. Development allows rapid iteration without affecting users, staging validates changes before release, and production serves actual traffic. Proper environment management ensures code flows smoothly through this pipeline while maintaining data integrity and security.

Setting Up Environment-Specific Configurations

WordPress 5.5 introduced native environment type support through the WP_ENVIRONMENT_TYPE constant. WP-CLI leverages this for environment-aware operations.

# Set environment type in wp-config.php
wp config set WP_ENVIRONMENT_TYPE 'development' --raw

# Verify current environment
wp config get WP_ENVIRONMENT_TYPE

# Different values: local, development, staging, production

Create environment-specific configuration files:

# wp-config-local.php for development
cat > wp-config-local.php << 'EOF'
<?php
// Development settings
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', true);
define('SCRIPT_DEBUG', true);
define('SAVEQUERIES', true);

// Disable caching in development
define('WP_CACHE', false);

// Development database
define('DB_NAME', 'wp_dev');
define('DB_USER', 'dev_user');
define('DB_PASSWORD', 'dev_password');
define('DB_HOST', 'localhost');
EOF

# wp-config-staging.php
cat > wp-config-staging.php << 'EOF'
<?php
// Staging settings
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

// Prevent search engine indexing
define('DISCOURAGE_SEARCH', true);

// Staging database
define('DB_NAME', 'wp_staging');
define('DB_USER', 'staging_user');
define('DB_PASSWORD', 'staging_password');
define('DB_HOST', 'localhost');
EOF

# wp-config-production.php
cat > wp-config-production.php << 'EOF'
<?php
// Production settings
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);

// Security enhancements
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', false);
define('FORCE_SSL_ADMIN', true);

// Production database
define('DB_NAME', 'wp_production');
define('DB_USER', 'prod_user');
define('DB_PASSWORD', 'secure_production_password');
define('DB_HOST', 'production-db.example.com');
EOF

Database Synchronization Strategies

Syncing databases between environments requires careful handling of environment-specific data like URLs, file paths, and credentials.

#!/bin/bash
# sync-db-staging-to-dev.sh - Sync staging database to development

set -euo pipefail

STAGING_PATH="/var/www/staging"
DEV_PATH="/var/www/development"
BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)

echo "Starting database sync from staging to development"

# Backup development database before sync
echo "Backing up development database..."
wp --path="$DEV_PATH" db export "$BACKUP_DIR/dev-pre-sync-$TIMESTAMP.sql"

# Export staging database
echo "Exporting staging database..."
wp --path="$STAGING_PATH" db export "$BACKUP_DIR/staging-export-$TIMESTAMP.sql"

# Get staging URL
STAGING_URL=$(wp --path="$STAGING_PATH" option get siteurl)

# Get development URL
DEV_URL=$(wp --path="$DEV_PATH" option get siteurl)

# Import to development
echo "Importing to development..."
wp --path="$DEV_PATH" db import "$BACKUP_DIR/staging-export-$TIMESTAMP.sql"

# Update URLs
echo "Updating URLs from $STAGING_URL to $DEV_URL"
wp --path="$DEV_PATH" search-replace "$STAGING_URL" "$DEV_URL" --all-tables

# Update file paths if different
STAGING_PATH_ABS=$(wp --path="$STAGING_PATH" eval 'echo ABSPATH;')
DEV_PATH_ABS=$(wp --path="$DEV_PATH" eval 'echo ABSPATH;')

if [ "$STAGING_PATH_ABS" != "$DEV_PATH_ABS" ]; then
    echo "Updating file paths..."
    wp --path="$DEV_PATH" search-replace "$STAGING_PATH_ABS" "$DEV_PATH_ABS" --all-tables
fi

# Flush cache
wp --path="$DEV_PATH" cache flush

# Update permalinks
wp --path="$DEV_PATH" rewrite flush

echo "Database sync completed successfully"

Selective Data Synchronization

Production databases often contain sensitive user data that shouldn’t be copied to development environments. Implement selective synchronization:

#!/bin/bash
# sync-selective.sh - Sync database with data sanitization

WP_PATH="/var/www/development"
TEMP_SQL="/tmp/staging-sanitized.sql"

# Export from staging (on staging server)
ssh staging@staging.example.com "wp --path=/var/www/html db export - " > "$TEMP_SQL"

# Import to development
wp --path="$WP_PATH" db import "$TEMP_SQL"

# Sanitize sensitive data in development
echo "Sanitizing user data..."

# Anonymize user emails (except admin)
wp --path="$WP_PATH" db query "
UPDATE wp_users
SET user_email = CONCAT('user', ID, '@example.local')
WHERE ID != 1
"

# Clear user passwords
wp --path="$WP_PATH" db query "
UPDATE wp_users
SET user_pass = MD5(CONCAT('dev_password_', ID))
"

# Remove personal data from comments
wp --path="$WP_PATH" db query "
UPDATE wp_comments
SET comment_author_email = CONCAT('commenter', comment_ID, '@example.local'),
    comment_author_IP = '127.0.0.1'
"

# Clear transients and cached data
wp --path="$WP_PATH" transient delete --all

# Update site URLs
wp --path="$WP_PATH" search-replace 'https://staging.example.com' 'http://dev.local' --all-tables

echo "Selective sync completed with data sanitization"
rm "$TEMP_SQL"

File Synchronization

Sync uploads and plugin/theme files between environments while excluding unnecessary files.

#!/bin/bash
# sync-files.sh - Sync WordPress files between environments

SOURCE_PATH="/var/www/staging"
DEST_PATH="/var/www/development"

# Sync uploads directory
echo "Syncing uploads..."
rsync -avz --delete \
    --exclude='cache/' \
    --exclude='backup/' \
    "$SOURCE_PATH/wp-content/uploads/" \
    "$DEST_PATH/wp-content/uploads/"

# Sync plugins (optional - usually managed by version control)
echo "Syncing plugins..."
rsync -avz --delete \
    --exclude='*/cache' \
    --exclude='*.log' \
    "$SOURCE_PATH/wp-content/plugins/" \
    "$DEST_PATH/wp-content/plugins/"

# Sync themes
echo "Syncing themes..."
rsync -avz --delete \
    "$SOURCE_PATH/wp-content/themes/" \
    "$DEST_PATH/wp-content/themes/"

# Set correct permissions
chown -R www-data:www-data "$DEST_PATH/wp-content"
find "$DEST_PATH/wp-content" -type d -exec chmod 755 {} \;
find "$DEST_PATH/wp-content" -type f -exec chmod 644 {} \;

echo "File sync completed"

Environment-Aware Deployment Scripts

Create intelligent deployment scripts that adapt based on target environment.

#!/bin/bash
# deploy.sh - Environment-aware deployment script

set -euo pipefail

# Configuration
ENVIRONMENTS=("development" "staging" "production")
TARGET_ENV="$1"

if [[ ! " ${ENVIRONMENTS[@]} " =~ " ${TARGET_ENV} " ]]; then
    echo "Error: Invalid environment. Use: development, staging, or production"
    exit 1
fi

# Load environment-specific configuration
case $TARGET_ENV in
    development)
        WP_PATH="/var/www/development"
        BACKUP_RETENTION=7
        UPDATE_CORE=true
        UPDATE_PLUGINS=true
        ;;
    staging)
        WP_PATH="/var/www/staging"
        BACKUP_RETENTION=14
        UPDATE_CORE=true
        UPDATE_PLUGINS=true
        ;;
    production)
        WP_PATH="/var/www/html"
        BACKUP_RETENTION=30
        UPDATE_CORE=false  # Manual approval required
        UPDATE_PLUGINS=false
        ;;
esac

echo "Deploying to: $TARGET_ENV"

# Pre-deployment checks
echo "Running pre-deployment checks..."

# Verify WordPress is installed
if ! wp --path="$WP_PATH" core is-installed; then
    echo "Error: WordPress not properly installed at $WP_PATH"
    exit 1
fi

# Check database connection
if ! wp --path="$WP_PATH" db check; then
    echo "Error: Database connection failed"
    exit 1
fi

# Create backup
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="/backups/$TARGET_ENV"
mkdir -p "$BACKUP_DIR"

echo "Creating backup..."
wp --path="$WP_PATH" db export "$BACKUP_DIR/pre-deploy-$TIMESTAMP.sql"

# Clean old backups
find "$BACKUP_DIR" -name "pre-deploy-*.sql" -mtime +$BACKUP_RETENTION -delete

# Maintenance mode
if [[ "$TARGET_ENV" == "production" ]]; then
    echo "Enabling maintenance mode..."
    wp --path="$WP_PATH" maintenance-mode activate
fi

# Deploy code (assuming Git deployment)
echo "Pulling latest code..."
cd "$WP_PATH"
git pull origin "$(git rev-parse --abbrev-ref HEAD)"

# Install/update dependencies
if [ -f "composer.json" ]; then
    composer install --no-dev --optimize-autoloader
fi

# Run database migrations if needed
if wp --path="$WP_PATH" plugin is-installed wp-migrate-db; then
    wp --path="$WP_PATH" migratedb migrate
fi

# Update plugins and core if allowed
if [[ "$UPDATE_PLUGINS" == true ]]; then
    echo "Updating plugins..."
    wp --path="$WP_PATH" plugin update --all --exclude=custom-plugin
fi

if [[ "$UPDATE_CORE" == true ]]; then
    echo "Updating WordPress core..."
    wp --path="$WP_PATH" core update --minor
fi

# Clear caches
echo "Clearing caches..."
wp --path="$WP_PATH" cache flush
wp --path="$WP_PATH" transient delete --expired

if wp --path="$WP_PATH" plugin is-installed wp-super-cache --status=active; then
    wp --path="$WP_PATH" cache flush
fi

# Regenerate assets if needed
if [ -f "package.json" ]; then
    npm run build
fi

# Update permalinks
wp --path="$WP_PATH" rewrite flush

# Deactivate maintenance mode
if [[ "$TARGET_ENV" == "production" ]]; then
    echo "Disabling maintenance mode..."
    wp --path="$WP_PATH" maintenance-mode deactivate
fi

# Post-deployment verification
echo "Running post-deployment verification..."

# Check site is accessible
SITE_URL=$(wp --path="$WP_PATH" option get siteurl)
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$SITE_URL")

if [ "$HTTP_CODE" != "200" ]; then
    echo "Warning: Site returned HTTP $HTTP_CODE"

    if [[ "$TARGET_ENV" == "production" ]]; then
        echo "Rolling back..."
        wp --path="$WP_PATH" db import "$BACKUP_DIR/pre-deploy-$TIMESTAMP.sql"
        exit 1
    fi
fi

# Verify core integrity
if ! wp --path="$WP_PATH" core verify-checksums; then
    echo "Warning: Core checksum verification failed"
fi

echo "Deployment to $TARGET_ENV completed successfully"

# Send notification (production only)
if [[ "$TARGET_ENV" == "production" ]]; then
    echo "Deployment completed at $(date)" | mail -s "Production Deployment Success" admin@example.com
fi

Configuration Management

Manage environment-specific settings without hardcoding sensitive values.

#!/bin/bash
# config-manager.sh - Environment configuration management

WP_PATH="/var/www/html"
ENV_FILE=".env"

# Load environment variables from .env file
if [ -f "$ENV_FILE" ]; then
    export $(cat "$ENV_FILE" | grep -v '^#' | xargs)
fi

# Set configuration based on environment
set_config() {
    local key="$1"
    local value="$2"
    local type="${3:-constant}"

    if [[ "$type" == "constant" ]]; then
        wp --path="$WP_PATH" config set "$key" "$value" --raw
    else
        wp --path="$WP_PATH" option update "$key" "$value"
    fi
}

# Get current environment
CURRENT_ENV=$(wp --path="$WP_PATH" config get WP_ENVIRONMENT_TYPE 2>/dev/null || echo "production")

echo "Configuring for environment: $CURRENT_ENV"

# Apply environment-specific settings
case $CURRENT_ENV in
    development|local)
        set_config WP_DEBUG true constant
        set_config WP_DEBUG_LOG true constant
        set_config WP_DEBUG_DISPLAY true constant
        set_config SCRIPT_DEBUG true constant
        set_config SAVEQUERIES true constant

        # Development plugins
        wp --path="$WP_PATH" plugin activate query-monitor
        wp --path="$WP_PATH" plugin activate debug-bar
        ;;

    staging)
        set_config WP_DEBUG true constant
        set_config WP_DEBUG_LOG true constant
        set_config WP_DEBUG_DISPLAY false constant
        set_config SCRIPT_DEBUG false constant

        # Disable search indexing
        set_config blog_public 0 option

        # Staging-specific plugins
        wp --path="$WP_PATH" plugin deactivate google-analytics
        ;;

    production)
        set_config WP_DEBUG false constant
        set_config WP_DEBUG_LOG false constant
        set_config WP_DEBUG_DISPLAY false constant
        set_config SCRIPT_DEBUG false constant

        # Security settings
        set_config DISALLOW_FILE_EDIT true constant
        set_config FORCE_SSL_ADMIN true constant

        # Enable search indexing
        set_config blog_public 1 option

        # Deactivate development plugins
        wp --path="$WP_PATH" plugin deactivate query-monitor --quiet 2>/dev/null || true
        wp --path="$WP_PATH" plugin deactivate debug-bar --quiet 2>/dev/null || true
        ;;
esac

echo "Configuration applied for $CURRENT_ENV environment"

Automated Environment Testing

Verify environment configuration and functionality before accepting deployments.

#!/bin/bash
# test-environment.sh - Automated environment testing

WP_PATH="/var/www/staging"

echo "=== Environment Testing ==="

# Test WordPress installation
echo "Testing WordPress installation..."
if wp --path="$WP_PATH" core is-installed; then
    echo "✓ WordPress is installed"
else
    echo "✗ WordPress installation check failed"
    exit 1
fi

# Test database connection
echo "Testing database connection..."
if wp --path="$WP_PATH" db check; then
    echo "✓ Database connection successful"
else
    echo "✗ Database connection failed"
    exit 1
fi

# Verify core files
echo "Verifying core files..."
if wp --path="$WP_PATH" core verify-checksums; then
    echo "✓ Core files verified"
else
    echo "⚠ Core checksum verification failed"
fi

# Check for required plugins
REQUIRED_PLUGINS=("wordpress-seo" "wordfence")
echo "Checking required plugins..."
for plugin in "${REQUIRED_PLUGINS[@]}"; do
    if wp --path="$WP_PATH" plugin is-installed "$plugin"; then
        STATUS=$(wp --path="$WP_PATH" plugin status "$plugin" 2>&1 | grep -o "Active" || echo "Inactive")
        echo "✓ $plugin: $STATUS"
    else
        echo "✗ $plugin: Not installed"
    fi
done

# Test URL accessibility
echo "Testing site accessibility..."
SITE_URL=$(wp --path="$WP_PATH" option get siteurl)
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$SITE_URL")

if [ "$HTTP_CODE" == "200" ]; then
    echo "✓ Site accessible (HTTP $HTTP_CODE)"
else
    echo "✗ Site returned HTTP $HTTP_CODE"
    exit 1
fi

# Check permalink structure
echo "Testing permalinks..."
wp --path="$WP_PATH" rewrite flush
if wp --path="$WP_PATH" rewrite list > /dev/null 2>&1; then
    echo "✓ Permalinks configured"
else
    echo "✗ Permalink configuration issue"
fi

# Performance checks
echo "Running performance checks..."
DB_SIZE=$(wp --path="$WP_PATH" db size --human-readable --format=json | jq -r '.Size')
echo "Database size: $DB_SIZE"

AUTOLOAD=$(wp --path="$WP_PATH" db query "SELECT CONCAT(ROUND(SUM(LENGTH(option_value))/1024/1024,2),'MB') as size FROM wp_options WHERE autoload='yes'" --skip-column-names)
echo "Autoload size: $AUTOLOAD"

echo "=== Environment Testing Complete ==="

Leave a Reply

Your email address will not be published. Required fields are marked *