WordPress Database Migration with MySQL 9.x in Docker: Complete Troubleshooting Guide
Executive Summary
Migrating WordPress databases in Docker environments has become increasingly complex with MySQL 9.x releases. This comprehensive guide documents real-world solutions for authentication issues, migration challenges, and platform-specific problems encountered across MySQL 9.1 through 9.4, with tested solutions for both AlmaLinux and Ubuntu environments.
The MySQL 9.x Challenge
What Changed and Why It Matters
Starting with MySQL 9.0, Oracle made fundamental changes to authentication mechanisms that broke many existing backup and migration workflows:
-- MySQL 8.x and earlier
mysql_native_password (deprecated but available)
caching_sha2_password (default)
-- MySQL 9.0+
mysql_native_password (COMPLETELY REMOVED)
caching_sha2_password (only option)
This seemingly small change has massive implications for Docker-based WordPress deployments, particularly affecting:
- Automated backup scripts
- Database migration procedures
- Cross-platform compatibility
- Legacy application support
Version-Specific Changes
MySQL 9.1 (October 2024)
- Initial release with complete
mysql_native_password
removal - Enhanced security requirements
- Known issues with Docker authentication
MySQL 9.2 – 9.3 (Late 2024 – Early 2025)
- Performance improvements
- Bug fixes for container environments
- Better ARM64 optimization
MySQL 9.4 (July 2025) – Latest
- Requires GCC 11 or later
- Dropped ARM support for RHEL7/CentOS7
- Improved Docker container integration
- Enhanced authentication handling
Real-World Migration Scenario
The Initial Problem
We encountered these symptoms when attempting a routine WordPress database migration:
# Standard approach that worked in MySQL 8.x
docker exec wordpress-db mysqldump -u root -pTestPass wordpress > backup.sql
# Result in MySQL 9.x:
mysqldump: Got error: 1045: Access denied for user 'root'@'localhost'
(using password: YES) when trying to connect
Discovery Process
Through extensive testing across different environments, we discovered:
- Ubuntu 22.04/24.04: Standard commands occasionally work
- AlmaLinux 9: Consistent authentication failures
- Docker Desktop (Windows/Mac): Intermittent issues
- Docker on ARM64: Additional platform-specific challenges
Platform-Specific Solutions
Solution 1: Universal Bypass Method (Works on All Platforms)
This approach bypasses MySQL 9.x authentication entirely by creating a temporary environment:
#!/bin/bash
# Universal MySQL 9.x Backup Solution
# Step 1: Prepare temporary environment
mkdir -p temp_mysql_data temp_run_mysqld
sudo cp -rp db_data/* temp_mysql_data/
# Step 2: Detect platform
PLATFORM="linux/amd64"
if [[ $(uname -m) == "aarch64" ]] || [[ $(uname -m) == "arm64" ]]; then
PLATFORM="linux/arm64/v8"
fi
# Step 3: Execute backup with authentication bypass
docker run --rm \
--platform ${PLATFORM} \
-v $(pwd)/temp_mysql_data:/var/lib/mysql:ro \
-v $(pwd)/temp_run_mysqld:/var/run/mysqld \
-v $(pwd)/backup:/backup \
mysql:9.4 \
bash -c '
# Start MySQL with authentication disabled
docker-entrypoint.sh mysqld \
--skip-grant-tables \
--skip-networking &
# Wait for MySQL to be ready
echo "Waiting for MySQL to initialize..."
for i in {1..60}; do
if mysqladmin ping --silent 2>/dev/null; then
echo "MySQL is ready"
break
fi
sleep 1
done
# Perform backup with MySQL 9.x compatible options
mysqldump \
--no-tablespaces \
--column-statistics=0 \
--skip-add-drop-table \
--single-transaction \
--quick \
--lock-tables=false \
--all-databases > /backup/full_backup_$(date +%Y%m%d_%H%M%S).sql
# Verify backup
if [ $? -eq 0 ]; then
echo "Backup completed successfully"
ls -lh /backup/full_backup_*.sql
else
echo "Backup failed"
exit 1
fi
# Clean shutdown
mysqladmin shutdown 2>/dev/null || true
sleep 5
'
# Step 4: Cleanup
sudo rm -rf temp_mysql_data temp_run_mysqld
Solution 2: Platform-Optimized Docker Compose
A more elegant solution using Docker Compose with platform detection:
# docker-compose.yml - MySQL 9.4 Compatible
version: '3.9'
services:
db:
image: mysql:9.4
container_name: wordpress-db
platform: ${DOCKER_PLATFORM:-linux/amd64}
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
# MySQL 9.x specific settings
MYSQL_ROOT_HOST: '%'
MYSQL_AUTHENTICATION_POLICY: 'caching_sha2_password'
volumes:
- db_data:/var/lib/mysql
- ./mysql-config:/etc/mysql/conf.d:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--protocol=TCP"]
interval: 30s
timeout: 15s
retries: 5
start_period: 120s # MySQL 9.x needs more time
networks:
- wordpress-net
ports:
- "3306:3306"
wordpress:
depends_on:
db:
condition: service_healthy
image: wordpress:6-apache
container_name: wordpress
platform: ${DOCKER_PLATFORM:-linux/amd64}
restart: unless-stopped
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: wordpress
WORDPRESS_CONFIG_EXTRA: |
define('WP_HOME', 'http://localhost:8080');
define('WP_SITEURL', 'http://localhost:8080');
volumes:
- wordpress_data:/var/www/html
- ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
networks:
- wordpress-net
backup:
image: mysql:9.4
container_name: wordpress-backup
platform: ${DOCKER_PLATFORM:-linux/amd64}
depends_on:
db:
condition: service_healthy
environment:
MYSQL_HOST: db
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- ./backup:/backup
- ./scripts:/scripts:ro
command: ["/scripts/backup.sh"]
networks:
- wordpress-net
volumes:
db_data:
wordpress_data:
networks:
wordpress-net:
driver: bridge
Step-by-Step Migration Process
Phase 1: Export from Source Database
#!/bin/bash
# export-database.sh - Works with MySQL 9.x
SOURCE_CONTAINER="wordpress-db"
BACKUP_FILE="wordpress_export_$(date +%Y%m%d_%H%M%S).sql"
# Method 1: Try standard export (might work on Ubuntu)
echo "Attempting standard export..."
if docker exec ${SOURCE_CONTAINER} mysqldump \
-u root -p${MYSQL_ROOT_PASSWORD} \
--no-tablespaces \
--column-statistics=0 \
wordpress > ${BACKUP_FILE} 2>/dev/null; then
echo "✓ Standard export successful"
else
echo "✗ Standard export failed, using bypass method..."
# Method 2: Use bypass method (always works)
./mysql9x-backup.sh
fi
# Verify export
if [ -f ${BACKUP_FILE} ]; then
echo "Export completed: ${BACKUP_FILE}"
echo "Size: $(ls -lh ${BACKUP_FILE} | awk '{print $5}')"
echo "Tables: $(grep -c 'CREATE TABLE' ${BACKUP_FILE})"
fi
Phase 2: Import to Target Database
#!/bin/bash
# import-database.sh - MySQL 9.x Compatible Import
TARGET_CONTAINER="wordpress-db-new"
IMPORT_FILE=$1
if [ -z "$IMPORT_FILE" ]; then
echo "Usage: $0 <import_file.sql>"
exit 1
fi
echo "Starting import process..."
# Step 1: Create database if not exists
docker exec ${TARGET_CONTAINER} mysql -u root -p${MYSQL_ROOT_PASSWORD} \
-e "CREATE DATABASE IF NOT EXISTS wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# Step 2: Import data
docker exec -i ${TARGET_CONTAINER} mysql -u root -p${MYSQL_ROOT_PASSWORD} wordpress < ${IMPORT_FILE}
# Step 3: Verify import
echo "Verifying import..."
docker exec ${TARGET_CONTAINER} mysql -u root -p${MYSQL_ROOT_PASSWORD} -e "
USE wordpress;
SELECT 'Tables' as Type, COUNT(*) as Count FROM information_schema.tables
WHERE table_schema = 'wordpress'
UNION ALL
SELECT 'Posts' as Type, COUNT(*) FROM wp_posts
UNION ALL
SELECT 'Users' as Type, COUNT(*) FROM wp_users;
"
echo "✓ Import completed successfully"
Phase 3: Fix Common Post-Migration Issues
Issue 1: Permalinks Not Working (404 Errors)
# Create proper .htaccess file
cat > wordpress_data/.htaccess << 'EOF'
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
EOF
# Set correct permissions
# First, identify the web server user ID
WEB_USER_ID=$(docker exec wordpress id -u www-data)
sudo chown ${WEB_USER_ID}:${WEB_USER_ID} wordpress_data/.htaccess
chmod 644 wordpress_data/.htaccess
# Enable mod_rewrite in Apache
docker exec wordpress a2enmod rewrite
docker restart wordpress
Issue 2: Database Connection Errors
-- Fix database URLs if site moved
UPDATE wp_options SET option_value = 'http://localhost:8080'
WHERE option_name IN ('siteurl', 'home');
-- Verify WordPress database settings
SELECT option_name, option_value
FROM wp_options
WHERE option_name IN ('siteurl', 'home', 'template', 'stylesheet');
Issue 3: Character Encoding Problems
# Export with proper encoding
docker exec wordpress-db mysqldump \
--default-character-set=utf8mb4 \
--no-tablespaces \
-u root -p${MYSQL_ROOT_PASSWORD} \
wordpress > backup_utf8mb4.sql
# Import with proper encoding
docker exec -i wordpress-db-new mysql \
--default-character-set=utf8mb4 \
-u root -p${MYSQL_ROOT_PASSWORD} \
wordpress < backup_utf8mb4.sql
Troubleshooting Common Issues
Authentication Failures in MySQL 9.x
# Diagnostic commands
docker exec wordpress-db mysql -u root -p${MYSQL_ROOT_PASSWORD} -e "
SELECT user, host, plugin FROM mysql.user WHERE user='root';
SHOW VARIABLES LIKE 'authentication_policy';
SHOW VARIABLES LIKE '%auth%';
"
# If authentication fails consistently, verify the container logs
docker logs wordpress-db 2>&1 | grep -i auth
Container Health Check Failures
# Enhanced health check for MySQL 9.x
healthcheck:
test: |
CMD mysql -h localhost -u root -p$$MYSQL_ROOT_PASSWORD -e "SELECT 1" || exit 1
interval: 30s
timeout: 20s
retries: 10
start_period: 180s # MySQL 9.x needs significant startup time
Volume Permission Issues
# Fix volume permissions for different platforms
# Ubuntu/Debian
sudo chown -R 999:999 ./db_data
# AlmaLinux/RHEL with SELinux
sudo chown -R 999:999 ./db_data
sudo chcon -Rt svirt_sandbox_file_t ./db_data
# macOS/Windows (Docker Desktop)
# Permissions are handled automatically by Docker Desktop
Automated Solutions
Complete Migration Script
#!/bin/bash
# migrate-wordpress.sh - Full WordPress Migration Script for MySQL 9.x
set -e
# Configuration
SOURCE_HOST="${1:-source-db}"
TARGET_HOST="${2:-target-db}"
BACKUP_DIR="./backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Create backup directory
mkdir -p ${BACKUP_DIR}
# Function to detect MySQL version
detect_mysql_version() {
local container=$1
docker exec ${container} mysql --version | grep -oP '\d+\.\d+\.\d+' | head -1
}
# Function to export database
export_database() {
local container=$1
local output_file=$2
log_info "Detecting MySQL version in ${container}..."
local version=$(detect_mysql_version ${container})
log_info "MySQL version: ${version}"
if [[ ${version} == 9.* ]]; then
log_warn "MySQL 9.x detected, using bypass method..."
# Create temporary directory
local temp_dir="temp_mysql_${TIMESTAMP}"
mkdir -p ${temp_dir}
# Copy database files
docker cp ${container}:/var/lib/mysql ${temp_dir}/
# Run bypass export
docker run --rm \
-v $(pwd)/${temp_dir}/mysql:/var/lib/mysql:ro \
-v $(pwd)/${BACKUP_DIR}:/backup \
mysql:${version} \
bash -c "
mysqld --skip-grant-tables --skip-networking &
sleep 10
mysqldump --all-databases --no-tablespaces --column-statistics=0 > /backup/${output_file}
mysqladmin shutdown
"
# Cleanup
rm -rf ${temp_dir}
else
log_info "Using standard export method..."
docker exec ${container} mysqldump \
--all-databases \
--no-tablespaces \
> ${BACKUP_DIR}/${output_file}
fi
log_info "Export completed: ${BACKUP_DIR}/${output_file}"
}
# Function to import database
import_database() {
local container=$1
local input_file=$2
log_info "Importing database to ${container}..."
docker exec -i ${container} mysql < ${BACKUP_DIR}/${input_file}
if [ $? -eq 0 ]; then
log_info "Import successful"
else
log_error "Import failed"
exit 1
fi
}
# Function to update WordPress URLs
update_wordpress_urls() {
local container=$1
local old_url=$2
local new_url=$3
log_info "Updating WordPress URLs from ${old_url} to ${new_url}..."
docker exec ${container} mysql -e "
USE wordpress;
UPDATE wp_options SET option_value = '${new_url}'
WHERE option_name IN ('siteurl', 'home');
UPDATE wp_posts SET guid = REPLACE(guid, '${old_url}', '${new_url}');
UPDATE wp_posts SET post_content = REPLACE(post_content, '${old_url}', '${new_url}');
UPDATE wp_postmeta SET meta_value = REPLACE(meta_value, '${old_url}', '${new_url}');
"
}
# Main migration process
main() {
log_info "Starting WordPress migration..."
# Export from source
export_database ${SOURCE_HOST} "wordpress_export_${TIMESTAMP}.sql"
# Import to target
import_database ${TARGET_HOST} "wordpress_export_${TIMESTAMP}.sql"
# Update URLs if needed
if [ ! -z "$3" ] && [ ! -z "$4" ]; then
update_wordpress_urls ${TARGET_HOST} $3 $4
fi
log_info "Migration completed successfully!"
}
# Run main function
main "$@"
Docker Compose for Testing
# docker-compose.test.yml - Test Environment
version: '3.9'
services:
# Source database (MySQL 8.x)
source-db:
image: mysql:8.4
environment:
MYSQL_ROOT_PASSWORD: sourcepass
MYSQL_DATABASE: wordpress
volumes:
- source_data:/var/lib/mysql
# Target database (MySQL 9.4)
target-db:
image: mysql:9.4
environment:
MYSQL_ROOT_PASSWORD: targetpass
MYSQL_DATABASE: wordpress
volumes:
- target_data:/var/lib/mysql
volumes:
source_data:
target_data:
Best Practices and Lessons Learned
Key Insights from Production Deployments
- Always Test Migration in Staging
# Create staging environmentdocker compose -f docker-compose.staging.yml up -d
# Test migration./migrate-wordpress.sh staging-source staging-target
# Verify functionalitycurl -I http://localhost:8081
- Version-Specific Handling
- MySQL 9.0-9.2: Major authentication issues
- MySQL 9.3: Improved but still problematic
- MySQL 9.4: Most stable, but bypass still needed
- Platform Considerations
- Ubuntu: Generally more compatible
- AlmaLinux/RHEL: Requires special handling
- Docker Desktop: Additional layer of complexity
- ARM64: Platform-specific images required
- Critical Success Factors
- Always verify backup integrity
- Test restore procedures regularly
- Monitor container health
- Keep authentication methods documented
Migration Checklist
- [ ] Backup source database
- [ ] Verify backup integrity
- [ ] Prepare target environment
- [ ] Test authentication methods
- [ ] Perform migration
- [ ] Update URLs and permalinks
- [ ] Fix file permissions
- [ ] Test all functionality
- [ ] Document any issues
- [ ] Create rollback plan
Performance Optimization
# Optimized MySQL 9.x configuration
services:
db:
image: mysql:9.4
command: |
--innodb_buffer_pool_size=1G
--innodb_log_file_size=256M
--innodb_flush_method=O_DIRECT
--innodb_file_per_table=1
--max_connections=200
--slow_query_log=1
--long_query_time=2
Conclusion
MySQL 9.x presents significant challenges for WordPress database migrations in Docker environments. The complete removal of mysql_native_password
authentication plugin requires new approaches and workarounds. The solutions presented here have been tested across multiple platforms and MySQL versions (9.1-9.4), providing reliable migration paths regardless of your environment.
Key Takeaways
- The bypass method works universally across all platforms and MySQL 9.x versions
- Platform-specific adjustments are necessary but manageable
- Automation is crucial for consistent, reliable migrations
- Testing is non-negotiable before production deployments
Future Considerations
As MySQL continues to evolve, we anticipate:
- Further authentication improvements in MySQL 10.x
- Better Docker integration
- Enhanced migration tools
- Improved cross-platform compatibility
Resources and References
- MySQL 9.4 Release Notes
- Docker MySQL Official Image
- WordPress Docker Documentation
- GitHub Repository with Complete Scripts
Last Updated: August 2025
Tested Environments:
- AlmaLinux 9, Ubuntu 22.04/24.04
- MySQL 9.1, 9.2, 9.3, 9.4
- Docker 24.x, Docker Compose v2.20+
- WordPress 6.x