8.2 KiB
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:
WORDPRESS_CONFIG_EXTRA: |
if (isset(\$_SERVER['HTTP_X_FORWARDED_PROTO'])) {
Correct:
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:
- "traefik.http.routers.site.rule=Host(`domain1.com`, `domain2.com`, `domain3.com`)"
Correct:
- "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:
- Disable the plugin BEFORE migration:
# 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
- Add proxy detection to wp-config.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:
- Start with a temporary domain (e.g., temp.example.com)
- Test everything thoroughly
- Only update to production domain after confirming everything works
- Keep both domains in Traefik configuration during transition
Database URL Update:
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:
# 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:
sudo cp -r /backup/wordpress ~/services/site/wordpress sudo chown -R www-data:www-data ~/services/site/wordpress - Copy database files:
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
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:
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