Automated WordPress site migration between VPS servers with Ansible, including Nginx configuration, SSL certificate provisioning, and database migration.
A production-ready toolkit for migrating WordPress sites from any source VPS to a target server with zero-downtime deployment, automatic SSL/TLS certificate provisioning via Let's Encrypt, and optimized Nginx configuration.
- Automated WordPress Migration - Complete site migration including database and
wp-contentdirectory - Intelligent Source Detection - Automatically locates WordPress installations on source servers
- Database Management - Creates isolated databases with secure credentials for each site
- Nginx Configuration - Production-ready virtual host configuration with performance optimizations
- SSL/TLS Automation - Let's Encrypt certificate provisioning via Certbot with HTTP-01 challenge
- Security Hardening - Includes catch-all vhost protection and security headers
- Per-Domain Logging - Separate access and error logs for each migrated domain
- Cloudflare Integration - Handles Cloudflare proxy mode requirements for SSL provisioning
- Zero Downtime - Migrates content before DNS cutover for seamless transitions
- Debian/Ubuntu-based Linux distribution
- Root or sudo access
- Python 3.6+
- Ansible 2.9+ (installed on target or control machine)
- SSH access with key-based authentication
- WP-CLI installed (for database export)
- Read access to WordPress files and database
- Access to DNS management (for A record updates)
- If using Cloudflare: ability to toggle proxy mode
-
Clone the repository
git clone https://github.com/Pawikoski/wp-migrator cd wp-migrator -
Install Ansible (if not already installed)
# Debian/Ubuntu apt update && apt install -y ansible # macOS brew install ansible
-
Configure Ansible (already set up in
ansible.cfg)# The project includes pre-configured ansible.cfg # Roles path: /opt/wp-migrator/roles
Edit sources.yml to define your source VPS servers:
sources:
sh01:
host: "0.0.0.0" # Source server IP or hostname
user: "username" # SSH user
port: 22 # SSH port
identity_file: "/root/.ssh/id_rsa" # SSH private key path
wp_path_suffix: "/home/username/domains/{domain}/public_html"Note: {domain} will be automatically replaced with the actual domain during migration.
Edit group_vars/all.yml:
# Target WordPress installation directory
wp_base_dir: /var/www/blogs
# Nginx paths
nginx_sites_available: /etc/nginx/sites-available
nginx_sites_enabled: /etc/nginx/sites-enabled
# PHP-FPM socket (adjust to your PHP version)
php_fpm_socket: /run/php/php8.4-fpm.sock
# Certbot configuration
enable_certbot: true
certbot_email: "admin@yourdomain.com"
# MariaDB/MySQL configuration
mysql_root_login_unix_socket: /run/mysqld/mysqld.sock
wp_db_admin_user: root
wp_db_admin_password: "" # Leave empty for Unix socket auth
# Cleanup source tmp files after migration
source_cleanup_tmp: falseIMPORTANT: Follow these steps in order to ensure successful SSL provisioning:
Point your domain's A record to the target VPS IP address:
Type: A
Name: @
Value: [target-vps-ip]
TTL: 300 (5 minutes)
If using Cloudflare, temporarily disable the proxy:
- Go to Cloudflare Dashboard → DNS
- Click the orange cloud icon to turn it gray (DNS only)
- This allows Let's Encrypt to verify domain ownership
./migrate.sh <source_name> <domain>Example:
./migrate.sh sh01 example.comThis will:
- ✅ Verify SSH connection to source VPS
- ✅ Auto-detect WordPress directory on source
- ✅ Create database and user on target VPS
- ✅ Export database from source VPS (via WP-CLI)
- ✅ Archive and transfer
wp-contentdirectory - ✅ Import database and files to target VPS
- ✅ Configure Nginx virtual host (HTTP)
- ✅ Provision SSL certificate via Let's Encrypt (if enabled)
- ✅ Update Nginx configuration to enable HTTPS
- ✅ Update WordPress site URLs to HTTPS
After successful migration:
- Click the gray cloud icon to turn it orange (Proxied)
- Set SSL/TLS mode to Full (strict)
If you've already migrated but need to add/renew SSL certificates:
./certbot.sh <domain>Example:
./certbot.sh example.comRequirements:
- Domain DNS must already point to target VPS
- Cloudflare proxy must be disabled (gray cloud)
- WordPress files must exist at
{{ wp_base_dir }}/{{ domain }}/public
WordPress Migrator works seamlessly with Cloudflare when configured correctly:
| Cloudflare Mode | Origin Certificate | Status |
|---|---|---|
| DNS Only (gray cloud) | Not required | ✅ Works |
| Proxied (orange cloud) + Flexible | Not required | |
| Proxied (orange cloud) + Full | Required | ✅ Recommended |
| Proxied (orange cloud) + Full (strict) | Required (Let's Encrypt) | ✅ Best security |
- During Migration: Gray cloud (DNS only)
- After Certbot: Orange cloud (Proxied) + Full (strict) mode
- Ongoing: Certificates auto-renew via systemd timer
-
521 Error: Origin server refused connection
- Cause: SSL not configured on origin while Cloudflare expects HTTPS
- Fix: Run
./certbot.sh domain.comand enable Full/Full (strict) mode
-
526 Error: Invalid SSL certificate
- Cause: Certificate expired or misconfigured
- Fix: Check certificate validity with
certbot certificates
Each migrated domain has dedicated log files:
# Access logs
tail -f /var/log/nginx/example.com.access.log
# Error logs
tail -f /var/log/nginx/example.com.error.log
# Follow both simultaneously
multitail /var/log/nginx/example.com.access.log /var/log/nginx/example.com.error.logCheck automatic certificate renewal:
# List all certificates
certbot certificates
# Test renewal process (dry run)
certbot renew --dry-run
# Check systemd timer status
systemctl status certbot.timer
systemctl list-timers | grep certbotThe migrator installs a default catch-all vhost that returns 444 (connection closed) for unrecognized domains. This prevents:
- Showing wrong site content for unmigrated domains
- Information disclosure
- Certificate errors
To disable this protection:
# In group_vars/all.yml
nginx_enable_default_deny: falseThe generated Nginx configuration includes:
- FastCGI Buffering: Optimized buffer sizes for PHP-FPM
- Static Asset Caching: 30-day cache for images, CSS, JS
- Gzip Compression: Reduces bandwidth usage
- Client Max Body Size: 64MB upload limit
- HTTP/2: Enabled when SSL is active
- Blocks direct access to
wp-config.php,readme.html,license.txt - Denies access to hidden files (
.htaccess,.git, etc.) - Prevents PHP execution in
wp-content/uploads/ - Sets
SCRIPT_FILENAMEto prevent path traversal
wp-migrator/
├── ansible.cfg # Ansible configuration
├── sources.yml # Source VPS definitions
├── group_vars/
│ └── all.yml # Global variables
├── playbooks/
│ ├── migrate.yml # Full migration playbook
│ ├── certbot.yml # Certificate-only playbook
│ └── vault/ # Encrypted database passwords (auto-generated)
├── roles/
│ ├── wp_site/ # Nginx + SSL configuration
│ │ ├── tasks/
│ │ │ ├── main.yml # Main site provisioning tasks
│ │ │ └── certbot.yml # SSL certificate tasks
│ │ └── templates/
│ │ ├── nginx-site.conf.j2
│ │ └── nginx-default-deny.conf.j2
│ └── wp_migrate/ # Database + file migration
│ ├── tasks/
│ │ └── main.yml # Migration tasks
│ └── defaults/
│ └── main.yml # Default variables
├── migrate.sh # Migration wrapper script
└── certbot.sh # Certificate wrapper script
Problem: Could not find wp-config.php on source VPS
Solution: The auto-detection failed. Manually specify the path in sources.yml:
sources:
sh01:
wp_path_suffix: "/full/absolute/path/to/wordpress" # Use absolute pathProblem: SSH connection failed
Solution: Verify SSH access manually:
ssh -i /path/to/key -p PORT user@host "whoami"Problem: Certbot did not issue certificate
Causes and Solutions:
- DNS not propagated: Wait for DNS TTL to expire, verify with
dig +short example.com - Cloudflare proxy enabled: Disable orange cloud (set to gray)
- Port 80 blocked: Ensure firewall allows HTTP (needed for HTTP-01 challenge)
- Webroot not accessible: Verify
{{ wp_base_dir }}/{{ domain }}/publicexists
Problem: Certificate already exists warning
Solution: Certbot detected existing cert. To force renewal:
certbot certonly --webroot -w /var/www/blogs/example.com/public \
-d example.com -d www.example.com --force-renewalProblem: nginx: [emerg] bind() to 0.0.0.0:443 failed (98: Address already in use)
Solution: Another process is using port 443:
# Find the process
lsof -i :443
# If it's another nginx, restart it
systemctl restart nginxProblem: Wrong site appears for domain
Solution: Check Nginx configuration:
# Test configuration
nginx -t
# Check which vhost handles the domain
curl -H "Host: example.com" http://127.0.0.1/
# Verify symlink exists
ls -la /etc/nginx/sites-enabled/ | grep example.comProblem: Access denied for user 'root'@'localhost'
Solution: Ensure MySQL root can authenticate via Unix socket:
# Test MySQL root access
mysql -u root -e "SELECT 1;"
# If fails, update group_vars/all.yml with password
wp_db_admin_password: "your_root_password"If using a different PHP version, update group_vars/all.yml:
php_fpm_socket: /run/php/php8.2-fpm.sock # Change version numberAdd multiple source definitions in sources.yml:
sources:
hosting01:
host: "192.168.1.10"
user: "webmaster"
port: 22
identity_file: "~/.ssh/hosting01_rsa"
wp_path_suffix: "/var/www/{domain}"
vps02:
host: "example.com"
user: "admin"
port: 2222
identity_file: "~/.ssh/vps02_ed25519"
wp_path_suffix: "/home/sites/{domain}/public_html"Then migrate from different sources:
./migrate.sh hosting01 site1.com
./migrate.sh vps02 site2.comTo use custom SSL certificates instead of Let's Encrypt:
# In group_vars/all.yml
enable_certbot: false
enable_ssl: true
ssl_cert_path: "/etc/ssl/certs/example.com.crt"
ssl_key_path: "/etc/ssl/private/example.com.key"- SSH Keys: Store private keys securely with
chmod 600 - Database Passwords: Auto-generated and stored in
playbooks/vault/ - Vault Encryption: Consider encrypting sensitive files:
ansible-vault encrypt sources.yml ansible-vault encrypt group_vars/all.yml
- Firewall: Configure UFW/iptables to allow only necessary ports (22, 80, 443)
- Regular Updates: Keep system packages and WordPress core updated
Contributions are welcome! Please feel free to submit a Pull Request.
# Clone the repository
git clone https://github.com/Pawikoski/wp-migrator.git
cd wp-migrator
# Create a feature branch
git checkout -b feature/your-feature-name
# Make changes and test
./migrate.sh test_source test.example.com
# Commit changes
git add .
git commit -m "Add: description of changes"
# Push and create PR
git push origin feature/your-feature-nameThis project is licensed under the MIT License - see the LICENSE file for details.
- Issues: Report bugs or request features via GitHub Issues
- Discussions: Join conversations in GitHub Discussions
- Built with Ansible
- SSL certificates provided by Let's Encrypt
- Powered by Nginx and PHP-FPM
Made with ❤️ for the WordPress community