WordPress Migration to VPS from Shared Hosting

Migrating WordPress from shared hosting to a VPS provides greater control, better performance, improved security, and more resources. However, migration requires careful planning to avoid downtime, data loss, or functionality issues. This comprehensive guide covers backup creation, LEMP stack setup on VPS, WordPress file transfer, database migration, DNS configuration, SSL/TLS setup, testing procedures, and optimization for the new environment.

Table of Contents

Pre-Migration Planning

Successful migration requires thorough planning and preparation.

Document current setup:

# Create comprehensive documentation
cat > /tmp/migration-plan.txt << 'EOF'
WordPress Site Migration Checklist

CURRENT SITE (Shared Hosting):
- Domain: example.com
- Hosting Provider: SharedHost Inc
- Current IP: 
- Current Database: 
- WordPress Version: 
- Plugins: (list all)
- Themes: (list all)
- Users: (count and roles)
- Content: Posts, Pages, Media (estimate sizes)
- Custom configurations: (any customizations)

NEW VPS:
- VPS Provider: VPS Provider Name
- OS: Ubuntu 22.04 LTS
- IP Address: (will be assigned)
- PHP Version: 8.2
- MySQL Version: 8.0
- Backup Location: /backups
- Test Domain: test.example.com (if possible)

MIGRATION TIMELINE:
- Backup creation: Date/Time
- LEMP setup: Date/Time
- File transfer: Date/Time
- Database migration: Date/Time
- Testing period: 24-48 hours
- DNS cutover: Date/Time (schedule during low traffic)
- Post-migration verification: Date/Time
EOF

cat /tmp/migration-plan.txt

Check WordPress for compatibility:

# Document current setup
# SSH into shared hosting first (if available)

# Check WordPress version
grep wp_version /path/to/wordpress/wp-includes/version.php

# List all plugins (via FTP/admin interface)
# List all themes
# Note any custom code or modifications

# Check PHP version requirements
# Document any special hosting features (caching, security modules, etc.)

Plan for minimal downtime:

# Schedule migration during low-traffic period
# Typically: 2-4 AM in your primary timezone
# Avoid peak business hours and marketing campaigns

# Plan for 1-2 hour maintenance window
# Communicate with users in advance if possible

Ensure you have VPS access credentials:

# You'll need:
# - VPS IP address
# - SSH credentials (root or sudo user)
# - Root/database password for MySQL
# - Any hosting-provided control panel access

Backing Up Your Existing Installation

Create comprehensive backups before beginning migration.

Access shared hosting via FTP/SFTP:

# Connect to shared hosting
sftp [email protected]

# Download entire WordPress directory
get -r public_html/wordpress /local/backup/wordpress-files/

# Download wp-config.php separately (contains credentials)
get public_html/wordpress/wp-config.php /local/backup/

# Exit FTP
bye

Or backup via hosting control panel:

# Via cPanel, Plesk, or other control panels:
# - Access File Manager
# - Select WordPress directory
# - Download as ZIP archive
# - Save to local machine

Backup database via phpMyAdmin:

# If you have SSH access:
mysqldump -u dbuser -p -h dbhost wordpress_db > /backup/wordpress-db.sql

# If using phpMyAdmin (web interface):
# - Select database
# - Export tab
# - Download as SQL file

Or backup via hosting control panel:

# Via control panel database backup feature:
# - Select database
# - Download SQL backup
# - Save locally

Create WordPress backup via plugin (if possible):

# If you have admin access to WordPress:
# Install UpdraftPlus or similar backup plugin
# Download backup files including database and files

Verify backup integrity:

# Check file sizes
du -sh /local/backup/wordpress-files/
ls -lh /local/backup/wordpress-db.sql

# Test database backup can be imported
mysql -u root -p test_db < /local/backup/wordpress-db.sql

# Check for errors
grep -i error /local/backup/wordpress-db.sql | head -10

Setting Up LEMP Stack on VPS

Install Linux, Nginx, MySQL, and PHP on your new VPS.

Initial VPS setup:

# SSH into VPS
ssh root@your-vps-ip

# Update system
apt update
apt upgrade -y

# Install basic utilities
apt install curl wget git zip unzip vim htop build-essential -y

Create application user:

# Create dedicated user for WordPress
useradd -m -s /bin/bash wordpress
usermod -aG sudo wordpress

# Set password
passwd wordpress

Install Nginx:

apt install nginx -y
systemctl start nginx
systemctl enable nginx
systemctl status nginx

Create web root:

# Create WordPress directory
mkdir -p /var/www/wordpress
chown -R wordpress:www-data /var/www/wordpress
chmod -R 755 /var/www/wordpress

Install MariaDB:

apt install mariadb-server mariadb-client -y
systemctl start mariadb
systemctl enable mariadb

# Secure installation
mysql_secure_installation
# Answer prompts (set root password, remove test database, etc.)

Create WordPress database and user:

mysql -u root -p << EOF
CREATE DATABASE wordpress_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'wordpress_user'@'localhost' IDENTIFIED BY 'SecurePassword123!';
GRANT ALL PRIVILEGES ON wordpress_db.* TO 'wordpress_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
EOF

Install PHP 8.2:

# Add PHP repository
apt install software-properties-common -y
add-apt-repository ppa:ondrej/php -y
apt update

# Install PHP and extensions
apt install php8.2 php8.2-fpm php8.2-mysql php8.2-curl php8.2-gd \
    php8.2-intl php8.2-json php8.2-xml php8.2-zip php8.2-mbstring \
    php8.2-opcache php8.2-cli php8.2-common -y

# Start PHP-FPM
systemctl start php8.2-fpm
systemctl enable php8.2-fpm

Configure Nginx for WordPress:

# Create WordPress Nginx configuration
cat > /etc/nginx/sites-available/wordpress.conf << 'EOF'
upstream php_backend {
    server unix:/run/php/php8.2-fpm.sock;
}

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/wordpress;
    index index.php index.html;

    access_log /var/log/nginx/wordpress_access.log;
    error_log /var/log/nginx/wordpress_error.log;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        fastcgi_pass php_backend;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location ~ /\. {
        deny all;
    }
}
EOF

# Enable configuration
ln -s /etc/nginx/sites-available/wordpress.conf /etc/nginx/sites-enabled/

# Test and reload
nginx -t
systemctl reload nginx

Transferring WordPress Files

Move WordPress files from shared hosting to VPS.

Prepare files for transfer:

# On shared hosting, create archive if not already done
# SSH into shared hosting
ssh [email protected]

# Create clean archive excluding unnecessary files
tar --exclude='wp-content/cache' \
    --exclude='wp-content/backup' \
    --exclude='.DS_Store' \
    -czf /tmp/wordpress-migration.tar.gz public_html/wordpress/

# Exit
exit

Download files to VPS:

# On VPS, download from shared hosting
cd /tmp
sftp [email protected] << EOF
get /tmp/wordpress-migration.tar.gz
bye
EOF

Or use rsync for large files (faster):

# From VPS, rsync from shared hosting
rsync -avz --delete -e ssh [email protected]:public_html/wordpress/ /var/www/wordpress/

# May take time for large installations
# Shows progress

Extract files on VPS:

# Extract archive
cd /var/www
tar xzf /tmp/wordpress-migration.tar.gz

# Move files to correct location (if extracted to subdirectory)
mv wordpress/* wordpress/
mv wordpress/.* wordpress/ 2>/dev/null || true

# Set permissions
chown -R www-data:www-data /var/www/wordpress
find /var/www/wordpress -type d -exec chmod 755 {} \;
find /var/www/wordpress -type f -exec chmod 644 {} \;

Verify file transfer:

# Check key files exist
ls -la /var/www/wordpress/wp-config.php
ls -la /var/www/wordpress/wp-includes/
ls -la /var/www/wordpress/wp-content/

# Count files
find /var/www/wordpress -type f | wc -l

Importing WordPress Database

Migrate the database from shared hosting to VPS.

Prepare clean database export:

# Export from shared hosting (ideally via SSH)
# Remove unnecessary WordPress data first for cleaner export

ssh [email protected] << EOF
# Connect to hosting database
mysqldump -u dbuser -p wordpress_db > /tmp/wordpress-clean.sql

# Optional: remove unwanted tables
# mysql -u dbuser -p wordpress_db << 'SQL'
# DROP TABLE wp_cache_table;
# SQL

exit
EOF

Transfer database file to VPS:

# Download database backup
sftp [email protected] << EOF
get /tmp/wordpress-clean.sql /tmp/wordpress-clean.sql
bye
EOF

# Or use scp
scp [email protected]:/tmp/wordpress-clean.sql /tmp/

Import database to VPS:

# Import database
mysql -u wordpress_user -p wordpress_db < /tmp/wordpress-clean.sql

# Verify import
mysql -u wordpress_user -p wordpress_db << EOF
SELECT COUNT(*) as table_count FROM information_schema.tables WHERE table_schema='wordpress_db';
SELECT COUNT(*) as post_count FROM wp_posts;
SELECT COUNT(*) as user_count FROM wp_users;
EOF

Update WordPress URLs in database:

# Update site URL from old domain to new VPS IP (temporarily)
mysql -u wordpress_user -p wordpress_db << EOF
UPDATE wp_options SET option_value='http://vps-ip-address' WHERE option_name='siteurl';
UPDATE wp_options SET option_value='http://vps-ip-address' WHERE option_name='home';
EOF

Updating WordPress Configuration

Modify wp-config.php for new VPS environment.

Update database credentials:

# Edit wp-config.php
nano /var/www/wordpress/wp-config.php

# Update these lines with VPS database info:
define('DB_NAME', 'wordpress_db');
define('DB_USER', 'wordpress_user');
define('DB_PASSWORD', 'SecurePassword123!');
define('DB_HOST', 'localhost');

Generate new security keys:

# Get fresh security keys from WordPress API
curl https://api.wordpress.org/secret-key/1.1/salt/

# Replace old keys in wp-config.php with new ones

Add VPS-specific settings:

# Add to wp-config.php before "That's all, stop editing":

// VPS environment settings
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');

// Force HTTPS
define('FORCE_SSL_ADMIN', true);
define('FORCE_SSL_LOGIN', true);

// Performance
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');

// Security
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', true);

// Debug logging
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);

Test configuration:

# Access WordPress via temporary URL
# Use VPS IP address: http://vps-ip-address

# Check for database connection errors
tail -f /var/www/wordpress/wp-content/debug.log

DNS Configuration

Point your domain to the VPS.

Update DNS nameservers (optional):

# If changing nameservers:
# 1. Log into domain registrar
# 2. Update nameservers to VPS provider's nameservers
# 3. Wait for propagation (24-48 hours)

# Verify DNS propagation
nslookup example.com
dig example.com

Or add A record to existing DNS:

# Log into DNS provider (Cloudflare, Route 53, etc.)
# Create/update A record:
# example.com    A    your-vps-ip-address
# www.example.com    A    your-vps-ip-address

# Test DNS resolution
nslookup example.com
# Should resolve to your VPS IP

# Check propagation
watch -n 5 'dig example.com +short'

Configure temporary test domain (optional):

# Before cutover, test with temporary domain pointing to VPS
# This allows testing without affecting live site

# Add to VPS Nginx config:
server {
    listen 80;
    server_name test.example.com;
    # ... rest of WordPress config ...
}

# Update hosts file on local machine:
echo "vps-ip-address test.example.com" >> /etc/hosts

# Test: curl http://test.example.com

SSL/TLS Certificate Setup

Secure WordPress with HTTPS certificate.

Install Certbot:

apt install certbot python3-certbot-nginx -y

Obtain SSL certificate:

# Before requesting certificate, ensure DNS points to VPS
# or use temporary domain for testing

certbot certonly --nginx -d example.com -d www.example.com

# Answer prompts
# Agree to terms, provide email
# Certificate installed to /etc/letsencrypt/live/example.com/

Update Nginx configuration for HTTPS:

# Edit WordPress configuration
nano /etc/nginx/sites-available/wordpress.conf

# Update to include SSL:
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # ... rest of WordPress config ...
}

Enable automatic renewal:

# Certbot automatic renewal is pre-configured
systemctl status certbot.timer

# Test renewal
certbot renew --dry-run

Update WordPress configuration:

# Ensure URLs use HTTPS
mysql -u wordpress_user -p wordpress_db << EOF
UPDATE wp_options SET option_value='https://example.com' WHERE option_name IN ('siteurl', 'home');
EOF

Verify HTTPS:

# Test HTTPS connection
curl -I https://example.com
# Should show 200 OK and SSL cert info

# Check mixed content
# Use browser developer console for warnings

Testing Before Going Live

Thoroughly test WordPress on VPS before DNS cutover.

Test WordPress functionality:

# Access WordPress admin
# https://vps-ip-address/wp-admin

# Check:
# - Admin login works
# - Dashboard displays correctly
# - Plugins load
# - Theme displays

# Test front-end
# - Homepage loads
# - Post/page viewing works
# - Search functionality
# - Comments system
# - Forms if applicable

Check plugin and theme functionality:

# Use WP-CLI to verify plugins
wp plugin list

# Activate any inactive plugins
wp plugin activate --all

# Check for errors
tail -f /var/www/wordpress/wp-content/debug.log

Verify media and uploads:

# Check if images load correctly
# - View posts with images
# - Test uploading new images
# - Verify image URLs resolve

# Check uploads directory
ls -la /var/www/wordpress/wp-content/uploads/

Test email functionality:

# Send test email
# Via WordPress: Settings > General > Email
# Or use mail function

# Install plugin to test
wp plugin install wp-mail-smtp --activate

# Configure and test

Test with multiple browsers:

# Test responsiveness
# Desktop browsers (Chrome, Firefox, Safari, Edge)
# Mobile browsers
# Check CSS loads
# Check JavaScript functions

# Use online tools:
# https://www.responsivedesignchecker.com/

Monitor logs for errors:

# Watch logs during testing
tail -f /var/log/nginx/wordpress_access.log
tail -f /var/log/nginx/wordpress_error.log
tail -f /var/www/wordpress/wp-content/debug.log
tail -f /var/log/php8.2-fpm.log

DNS Cutover and Monitoring

Switch live traffic to VPS and monitor for issues.

Schedule DNS cutover:

# Choose low-traffic time window
# Update DNS to point to VPS IP
# TTL should be lowered 24-48 hours before

# Typical process:
# 1. Lower TTL to 300 seconds (5 minutes) 24 hours before
# 2. At scheduled time, update A record to VPS IP
# 3. Monitor DNS propagation
# 4. Monitor site traffic and errors

Monitor DNS propagation:

# Check propagation across different regions
# Use: https://www.whatsmydns.net/

# Command line check
nslookup example.com

# Watch for propagation
watch -n 5 'dig example.com +short'

Monitor site during cutover:

# Watch web server logs
tail -f /var/log/nginx/wordpress_access.log

# Watch error logs
tail -f /var/log/nginx/wordpress_error.log

# Monitor server resources
top
free -h

# Check page load times
curl -o /dev/null -s -w "Total: %{time_total}s\n" https://example.com

Verify old hosting is receiving no traffic:

# After DNS propagation, old hosting should receive no WordPress traffic
# Keep old hosting for few days during transition period
# Monitor error logs - should be minimal/none

# Then delete old hosting account (keep backup for record)

Update hardcoded URLs in database (if needed):

# Some plugins/themes may have hardcoded URLs
# Search and replace in database

# Using WP-CLI search-replace
wp search-replace 'http://old-ip-address' 'https://example.com'
wp search-replace 'http://shared-hosting.com' 'https://example.com'

# Verify replacements
wp search-replace 'http://shared-hosting' '' --dry-run

Conclusion

Migrating WordPress from shared hosting to VPS involves careful planning, methodical execution, and thorough testing. This guide covers the complete process from backup creation through DNS cutover. Key success factors include comprehensive backups before starting, setting up identical LEMP stack configuration, careful file and database transfer, proper configuration updates, SSL/TLS setup, and thorough testing using temporary domains before cutover. Scheduling the DNS cutover during low-traffic periods and maintaining access to old hosting during the transition period provides safety net for quick rollback if issues arise. Following this systematic approach ensures minimal downtime and data integrity during migration.