# WordPress Docker Migration Guide - Lessons Learned **Last Updated**: 2025-09-19 **Based on**: BestSolarTech.com migration experience ## Critical Gotchas & Solutions ### 1. Variable Escaping in Docker Compose Environment Variables **Problem**: `$_SERVER` variables in WORDPRESS_CONFIG_EXTRA are interpreted as shell variables, causing warnings and broken configuration. **Symptom**: ``` level=warning msg="The \"_SERVER\" variable is not set. Defaulting to a blank string." ``` **Wrong**: ```yaml WORDPRESS_CONFIG_EXTRA: | if (isset(\$_SERVER['HTTP_X_FORWARDED_PROTO'])) { ``` **Correct**: ```yaml WORDPRESS_CONFIG_EXTRA: | if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO'])) { ``` **Solution**: Use `$$` to escape dollar signs in Docker Compose environment variables. ### 2. Traefik v3 Host Rule Syntax **Problem**: Multiple hosts in Traefik labels don't work with comma separation. **Wrong**: ```yaml - "traefik.http.routers.site.rule=Host(`domain1.com`, `domain2.com`, `domain3.com`)" ``` **Correct**: ```yaml - "traefik.http.routers.site.rule=Host(`domain1.com`) || Host(`domain2.com`) || Host(`domain3.com`)" ``` **Solution**: Use `||` (OR operator) to separate multiple hosts in Traefik v3. ### 3. MariaDB Version Compatibility **Problem**: MariaDB version mismatches cause redo log format errors. **Error**: ``` [ERROR] InnoDB: Unsupported redo log format. The redo log was created with MariaDB 10.11.14. ``` **Solution**: - Check the original database version before migration - Use the same or newer MariaDB version - MariaDB 10.11 is a safe choice for most WordPress sites ### 4. Real Simple SSL Plugin Behind Traefik **Problem**: Real Simple SSL plugin causes redirect loops when WordPress is behind Traefik reverse proxy. **Solution**: 1. Disable the plugin BEFORE migration: ```bash # Via database (serialized array - be careful!) UPDATE wp_options SET option_value = 'a:0:{}' WHERE option_name = 'active_plugins'; # OR use WP-CLI after migration docker exec CONTAINER wp --allow-root plugin deactivate really-simple-ssl ``` 2. Add proxy detection to wp-config.php: ```php /* Handle SSL behind reverse proxy */ if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $_SERVER['HTTPS'] = 'on'; $_SERVER['SERVER_PORT'] = 443; } define('FORCE_SSL_ADMIN', true); ``` ### 5. WordPress URL Management Strategy **Problem**: Changing URLs too early causes redirect loops and access issues. **Best Practice**: 1. Start with a temporary domain (e.g., temp.example.com) 2. Test everything thoroughly 3. Only update to production domain after confirming everything works 4. Keep both domains in Traefik configuration during transition **Database URL Update**: ```sql UPDATE wp_options SET option_value = 'https://yourdomain.com' WHERE option_name IN ('siteurl', 'home'); ``` ### 6. Database Password Reset for Existing Databases **Problem**: Existing database has unknown root password. **Solution** - Use skip-grant-tables: ```bash # Start MariaDB with skip-grant-tables docker run -d --name temp_db -v ~/services/SERVICE/db_data:/var/lib/mysql mariadb:10.11 --skip-grant-tables # Reset passwords docker exec temp_db mysql -e " USE mysql; FLUSH PRIVILEGES; ALTER USER 'root'@'%' IDENTIFIED BY 'new_password'; CREATE USER IF NOT EXISTS 'wordpress'@'%' IDENTIFIED BY 'wp_password'; GRANT ALL PRIVILEGES ON dbname.* TO 'wordpress'@'%'; FLUSH PRIVILEGES; " # Stop temp container and start normal service docker stop temp_db && docker rm temp_db ``` ## Complete Migration Checklist ### Pre-Migration - [ ] Document current WordPress version, PHP version, active theme - [ ] List all active plugins (especially security/SSL plugins) - [ ] Check database size and MariaDB version - [ ] Note table prefix (often not default `wp_`) - [ ] Create full backup of files and database ### File Migration - [ ] Copy WordPress files preserving structure: ```bash sudo cp -r /backup/wordpress ~/services/site/wordpress sudo chown -R www-data:www-data ~/services/site/wordpress ``` - [ ] Copy database files: ```bash sudo cp -r /backup/mysql ~/services/site/db_data sudo chown -R 999:999 ~/services/site/db_data ``` - [ ] Copy/create uploads.ini for PHP settings ### Docker Compose Configuration - [ ] Use correct MariaDB version (10.11 recommended) - [ ] Escape `$` signs as `$$` in environment variables - [ ] Use `||` for multiple hosts in Traefik rules - [ ] Include both internal and traefik networks - [ ] Set health check for database - [ ] Configure volumes for persistence ### WordPress Configuration - [ ] Update wp-config.php database host to 'mariadb:3306' - [ ] Add proxy SSL detection code - [ ] Remove hardcoded URLs (use $_SERVER['HTTP_HOST']) - [ ] Disable SSL plugins before starting ### Testing Phase - [ ] Start with temporary domain - [ ] Test database connectivity - [ ] Verify no redirect loops - [ ] Check admin panel access - [ ] Test file uploads - [ ] Verify all plugins working ### Production Cutover - [ ] Update DNS records to new server - [ ] Update database URLs to production domain - [ ] Test SSL certificate generation - [ ] Monitor logs for errors - [ ] Have rollback plan ready ## Sample Docker Compose Template ```yaml services: wordpress: image: wordpress:6.7-php8.3-apache container_name: ${SITE_NAME}_wp restart: unless-stopped environment: WORDPRESS_DB_HOST: mariadb:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: ${DB_PASSWORD} WORDPRESS_DB_NAME: ${DB_NAME} WORDPRESS_TABLE_PREFIX: ${TABLE_PREFIX:-wp_} WORDPRESS_CONFIG_EXTRA: | /* Handle SSL behind reverse proxy */ if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $$_SERVER['HTTPS'] = 'on'; $$_SERVER['SERVER_PORT'] = 443; } define('FORCE_SSL_ADMIN', true); define('WP_MEMORY_LIMIT', '256M'); volumes: - ./wordpress:/var/www/html - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro networks: - internal - traefik depends_on: mariadb: condition: service_healthy labels: - "traefik.enable=true" - "traefik.docker.network=traefik" - "traefik.http.routers.${SITE_NAME}.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.${SITE_NAME}.entrypoints=web" - "traefik.http.routers.${SITE_NAME}.middlewares=redirect-to-https@file" - "traefik.http.routers.${SITE_NAME}-secure.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.${SITE_NAME}-secure.entrypoints=websecure" - "traefik.http.routers.${SITE_NAME}-secure.tls=true" - "traefik.http.routers.${SITE_NAME}-secure.tls.certresolver=letsencrypt" - "traefik.http.services.${SITE_NAME}.loadbalancer.server.port=80" mariadb: image: mariadb:10.11 container_name: ${SITE_NAME}_db restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} MYSQL_DATABASE: ${DB_NAME} MYSQL_USER: wordpress MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - ./db_data:/var/lib/mysql networks: - internal healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 5 start_period: 60s networks: traefik: external: true internal: external: true ``` ## WP-CLI Installation Install WP-CLI in WordPress container for management: ```bash docker exec CONTAINER bash -c 'curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && chmod +x wp-cli.phar && mv wp-cli.phar /usr/local/bin/wp' # Use with --allow-root flag docker exec CONTAINER wp --allow-root plugin list docker exec CONTAINER wp --allow-root cache flush ``` ## Quick Troubleshooting | Problem | Solution | |---------|----------| | 404 errors from Traefik | Restart Traefik: `docker restart traefik` | | Redirect loops | Check database URLs match expected domain | | SSL certificate errors | Check DNS points to server, wait for Let's Encrypt | | Database connection failed | Verify credentials, check container network | | Plugin conflicts | Disable via database or WP-CLI | ## Important Notes - Always test on a temporary domain first - Keep backups before making changes - Document all credentials securely - Monitor logs during migration - Have a rollback plan ready