Migration from Shared Hosting to VPS: Complete Guide
Migrating from shared hosting to a Virtual Private Server (VPS) requires careful planning and execution. This comprehensive guide covers VPS setup, cPanel backup extraction, file transfer, database import, email migration, DNS configuration, and SSL certificate transition for a complete managed migration.
Table of Contents
- Migration Planning
- VPS Setup and Preparation
- cPanel Backup Extraction
- File Transfer Methods
- Database Migration
- Email Migration
- DNS Configuration
- SSL Certificate Migration
- Testing and Verification
- Conclusion
Migration Planning
Create Migration Timeline
# Migration planning template
cat > /tmp/migration-plan.md << 'EOF'
# Shared Hosting to VPS Migration Plan
## Pre-Migration Phase (1 week before)
- [ ] Assess current hosting environment
- [ ] Document all domains and accounts
- [ ] List all databases
- [ ] Export email accounts
- [ ] Document DNS records
- [ ] Identify custom scripts/configurations
- [ ] Test VPS infrastructure
## Cutover Window (Maintenance window)
- [ ] Backup all data on shared hosting
- [ ] Stop application writes
- [ ] Export final databases
- [ ] Final file sync
- [ ] Update DNS
- [ ] Verify all services
- [ ] Monitor for errors
## Post-Migration Phase (1 week after)
- [ ] Monitor email delivery
- [ ] Check application functionality
- [ ] Verify database integrity
- [ ] Test SSL certificates
- [ ] Monitor DNS propagation
- [ ] Remove data from old host
- [ ] Document lessons learned
## Estimated Downtime: 1-4 hours
## Risk Level: Medium
EOF
cat /tmp/migration-plan.md
Inventory Current Setup
# Document shared hosting environment
document_shared_hosting() {
local report_file="/tmp/hosting-inventory-$(date +%Y%m%d).txt"
cat > "$report_file" << 'EOF'
SHARED HOSTING INVENTORY REPORT
================================
DOMAINS & ACCOUNTS
------------------
Domain Name: [example.com]
Addon Domains: [list]
Subdomains: [list]
Primary Account: [username]
FTP Accounts: [count]
Email Accounts: [count]
DATABASE INFORMATION
--------------------
Database System: [MySQL/MariaDB]
Databases: [names]
Total Size: [GB]
Tables Count: [number]
User Accounts: [list]
FILE STRUCTURE
--------------
Public HTML: [size/count]
Backups: [location/size]
Logs: [location]
Custom Scripts: [list]
EMAIL SETUP
-----------
Email Accounts: [count]
Forwarding Rules: [count]
Autoresponders: [count]
Backup MX Records: [yes/no]
SSL CERTIFICATES
----------------
Certificate Type: [self-signed/purchased]
Domain Covered: [domain/wildcard]
Expiration Date: [date]
Issuer: [CA name]
CUSTOM CONFIGURATIONS
---------------------
.htaccess Rules: [yes/no - detailed]
Custom Headers: [yes/no]
Redirects: [yes/no - detailed]
User Agents Rules: [yes/no]
IP Restrictions: [yes/no]
EOF
cat "$report_file"
echo ""
echo "Inventory report saved to: $report_file"
}
document_shared_hosting
VPS Setup and Preparation
Initial VPS Configuration
# Initial VPS setup checklist
setup_vps() {
echo "VPS Setup Checklist"
echo "===================="
# 1. Update system packages
apt-get update && apt-get upgrade -y
# 2. Install essential services
apt-get install -y \
build-essential \
curl \
wget \
git \
openssh-server \
fail2ban \
ufw
# 3. Configure firewall
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 25/tcp
ufw allow 587/tcp
ufw enable
# 4. Install web server and PHP
apt-get install -y \
apache2 \
php \
php-mysql \
php-fpm \
libapache2-mod-php
# 5. Install database
apt-get install -y mysql-server
# 6. Install mail server
apt-get install -y \
postfix \
dovecot-core \
dovecot-imapd
# 7. Create required directories
mkdir -p /home/vps/domains
mkdir -p /home/vps/backups
mkdir -p /home/vps/emails
echo "✓ VPS base setup completed"
}
# Configure Apache virtual hosts
configure_apache_vhosts() {
local domain=$1
local doc_root="/home/vps/domains/$domain/public_html"
mkdir -p "$doc_root"
# Create virtual host configuration
cat > "/etc/apache2/sites-available/$domain.conf" << EOF
<VirtualHost *:80>
ServerName $domain
ServerAlias www.$domain
ServerAdmin admin@$domain
DocumentRoot $doc_root
<Directory $doc_root>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/$domain-error.log
CustomLog \${APACHE_LOG_DIR}/$domain-access.log combined
</VirtualHost>
<VirtualHost *:443>
ServerName $domain
ServerAlias www.$domain
ServerAdmin admin@$domain
DocumentRoot $doc_root
SSLEngine on
SSLCertificateFile /etc/ssl/certs/$domain.crt
SSLCertificateKeyFile /etc/ssl/private/$domain.key
<Directory $doc_root>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/$domain-error.log
CustomLog \${APACHE_LOG_DIR}/$domain-access.log combined
</VirtualHost>
EOF
# Enable site and modules
a2ensite "$domain"
a2enmod rewrite
a2enmod ssl
apache2ctl configtest
systemctl restart apache2
}
cPanel Backup Extraction
Extract cPanel Backups
# Download and extract cPanel backup
extract_cpanel_backup() {
local backup_url=$1
local backup_file="backup.tar.gz"
local extract_dir="/tmp/cpanel-backup"
echo "Downloading cPanel backup..."
# Download backup (requires proper authentication)
wget -O "$backup_file" "$backup_url"
if [ $? -ne 0 ]; then
echo "Error: Failed to download backup"
return 1
fi
# Create extraction directory
mkdir -p "$extract_dir"
# Extract backup
echo "Extracting backup..."
tar -xzf "$backup_file" -C "$extract_dir"
if [ $? -eq 0 ]; then
echo "✓ Backup extracted to: $extract_dir"
# List backup contents
ls -la "$extract_dir"
fi
}
# Extract specific domain from cPanel backup
extract_domain_from_cpanel() {
local backup_file=$1
local domain_name=$2
local extract_dir="/tmp/cpanel-$domain_name"
echo "Extracting $domain_name from cPanel backup..."
# Create extraction directory
mkdir -p "$extract_dir"
# Extract only specific domain
tar -xzf "$backup_file" \
-C "$extract_dir" \
--wildcards \
"*${domain_name}*" \
--transform="s|^[^/]*/||" 2>/dev/null
# Alternative: Extract all and filter
tar -xzf "$backup_file" -C "$extract_dir"
echo "✓ Domain extracted to: $extract_dir"
}
# Parse cPanel backup structure
parse_cpanel_backup_structure() {
local backup_dir=$1
echo "cPanel Backup Structure Analysis"
echo "=================================="
if [ -d "$backup_dir/public_html" ]; then
echo "✓ Website files: $backup_dir/public_html"
du -sh "$backup_dir/public_html"
fi
if [ -f "$backup_dir/mysql.sql" ]; then
echo "✓ MySQL backup: $backup_dir/mysql.sql"
ls -lh "$backup_dir/mysql.sql"
fi
if [ -d "$backup_dir/mail" ]; then
echo "✓ Email data: $backup_dir/mail"
du -sh "$backup_dir/mail"
fi
if [ -f "$backup_dir/.htaccess" ]; then
echo "✓ Apache configuration: $backup_dir/.htaccess"
fi
if [ -d "$backup_dir/cgi-bin" ]; then
echo "✓ CGI scripts: $backup_dir/cgi-bin"
fi
}
File Transfer Methods
High-Speed File Transfer Setup
# Configure rsync for efficient file transfer
transfer_files_with_rsync() {
local source_host=$1
local source_dir=$2
local dest_dir=$3
echo "Transferring files with rsync..."
rsync -avz \
--progress \
--checksum \
--partial \
--bwlimit=50000 \
--exclude='.cache' \
--exclude='.tmp' \
--exclude='logs/*' \
"$source_host:$source_dir/" \
"$dest_dir/"
if [ $? -eq 0 ]; then
echo "✓ File transfer completed"
else
echo "✗ File transfer failed"
return 1
fi
}
# FTP-based file transfer (slower, but works everywhere)
transfer_files_with_sftp() {
local shared_host=$1
local sftp_user=$2
local sftp_pass=$3
local local_dir=$4
echo "Transferring files with SFTP..."
# Using lftp for batch SFTP transfers
lftp -u "$sftp_user,$sftp_pass" \
"sftp://$shared_host" << EOF
set sftp:auto-confirm yes
mirror --use-cache --parallel=4 /public_html/ $local_dir/
quit
EOF
if [ $? -eq 0 ]; then
echo "✓ SFTP transfer completed"
fi
}
# Verify transferred files
verify_transferred_files() {
local source_dir=$1
local dest_dir=$2
echo "Verifying file transfer..."
# Compare file counts
source_count=$(find "$source_dir" -type f | wc -l)
dest_count=$(find "$dest_dir" -type f | wc -l)
echo "Source files: $source_count"
echo "Destination files: $dest_count"
if [ "$source_count" -eq "$dest_count" ]; then
echo "✓ File count matches"
else
echo "⚠ File count mismatch"
fi
# Compare directory size
source_size=$(du -sb "$source_dir" | awk '{print $1}')
dest_size=$(du -sb "$dest_dir" | awk '{print $1}')
echo "Source size: $((source_size / 1024 / 1024))MB"
echo "Destination size: $((dest_size / 1024 / 1024))MB"
# Calculate file checksums for sample files
find "$source_dir" -type f -name "*.php" -o -name "*.html" | head -10 | while read file; do
source_hash=$(sha256sum "$file" | awk '{print $1}')
dest_file="${file#$source_dir}"; dest_file="$dest_dir$dest_file"
if [ -f "$dest_file" ]; then
dest_hash=$(sha256sum "$dest_file" | awk '{print $1}')
if [ "$source_hash" = "$dest_hash" ]; then
echo "✓ $(basename "$file"): matches"
else
echo "✗ $(basename "$file"): mismatch"
fi
fi
done
}
Database Migration
Export and Import MySQL Databases
# Export databases from shared hosting
export_shared_hosting_databases() {
local ssh_host=$1
local db_user=$2
local db_password=$3
local backup_dir="/tmp/mysql-backup"
mkdir -p "$backup_dir"
echo "Exporting databases from shared hosting..."
# Export all databases
ssh "root@$ssh_host" << EOF
mysqldump -u $db_user -p$db_password --all-databases --single-transaction --quick > /home/backups/all-databases.sql
EOF
# Download export
scp "root@$ssh_host:/home/backups/all-databases.sql" "$backup_dir/"
if [ $? -eq 0 ]; then
echo "✓ Databases exported successfully"
ls -lh "$backup_dir/all-databases.sql"
fi
}
# Import databases on VPS
import_databases_to_vps() {
local backup_file=$1
local db_root_pass=$2
echo "Importing databases to VPS..."
# Create MySQL backup
mysql -u root -p"$db_root_pass" << EOF
-- Create new databases and users
CREATE DATABASE IF NOT EXISTS migrated_db;
GRANT ALL PRIVILEGES ON migrated_db.* TO 'app_user'@'localhost' IDENTIFIED BY 'secure_password';
FLUSH PRIVILEGES;
EOF
# Import database dump
if [[ "$backup_file" == *.gz ]]; then
gunzip < "$backup_file" | mysql -u root -p"$db_root_pass"
else
mysql -u root -p"$db_root_pass" < "$backup_file"
fi
if [ $? -eq 0 ]; then
echo "✓ Databases imported successfully"
# Verify import
mysql -u root -p"$db_root_pass" -e "SHOW DATABASES;" | grep -v "^|"
fi
}
# Migrate specific database
migrate_specific_database() {
local source_db=$1
local source_host=$2
local source_user=$3
local source_pass=$4
echo "Migrating database: $source_db"
# Remote dump
ssh "root@$source_host" << EOF
mysqldump -u $source_user -p$source_pass $source_db > /tmp/$source_db.sql
EOF
# Download
scp "root@$source_host:/tmp/$source_db.sql" /tmp/
# Import locally
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS $source_db;"
mysql -u root -p "$source_db" < "/tmp/$source_db.sql"
echo "✓ Database migration completed"
}
Email Migration
Migrate Email Accounts
# Extract email data from cPanel
extract_cpanel_emails() {
local cpanel_backup=$1
local email_extract_dir="/tmp/email-backup"
echo "Extracting email data from cPanel backup..."
mkdir -p "$email_extract_dir"
# Extract mail directory
tar -xzf "$cpanel_backup" \
-C "$email_extract_dir" \
--wildcards \
"*/mail*" \
2>/dev/null
# Find mail directories
find "$email_extract_dir" -name "mail" -type d | head -10
}
# Create email accounts on new VPS
create_email_accounts() {
local email_list=$1
echo "Creating email accounts..."
# Read email accounts from file/list
while IFS=: read -r email password domain user; do
echo "Creating: $email"
# Create mail directory
mkdir -p "/home/mail/$domain/$user"
chown mail:mail "/home/mail/$domain/$user"
chmod 700 "/home/mail/$domain/$user"
# Create Maildir structure (for IMAP)
mkdir -p "/home/mail/$domain/$user/Maildir"/{new,cur,tmp}
chown -R mail:mail "/home/mail/$domain/$user/Maildir"
chmod -R 700 "/home/mail/$domain/$user/Maildir"
# Add user to mail system (example with Dovecot)
# doveadm user add "$email"
done < "$email_list"
}
# Migrate mailbox data
migrate_mailbox_data() {
local source_maildir=$1
local dest_maildir=$2
echo "Migrating mailbox data..."
# Use rsync for mailbox migration
rsync -avz \
--progress \
--delete \
"$source_maildir/" \
"$dest_maildir/"
# Fix permissions
chown -R mail:mail "$dest_maildir"
chmod -R 700 "$dest_maildir"
echo "✓ Mailbox migration completed"
}
# Test mail delivery
test_mail_migration() {
local test_email=$1
echo "Testing email setup..."
# Test SMTP
telnet localhost 25
# Test IMAP
telnet localhost 143
# Send test email
echo "Test email" | mail -s "Migration Test" "$test_email"
echo "✓ Mail system tested"
}
DNS Configuration
Update DNS Records
# Plan DNS migration with low TTL
prepare_dns_migration() {
local domain=$1
echo "DNS Migration Plan for: $domain"
echo "================================="
echo ""
echo "1. Lower TTL (24 hours before migration)"
echo " Current TTL: [check with dig]"
echo " New TTL: 300 seconds (5 minutes)"
echo ""
echo "2. Update NS records (at registrar)"
echo " Current NS: [check with dig]"
echo " New NS: [VPS nameserver IPs]"
echo ""
echo "3. Update A records"
echo " Old IP: [current shared hosting]"
echo " New IP: [VPS IP]"
echo ""
echo "4. Wait for propagation"
echo " Monitor with: watch -n 5 'dig @8.8.8.8 $domain'"
echo ""
echo "5. Verify all record types"
echo " - A records"
echo " - CNAME records"
echo " - MX records"
echo " - TXT records (SPF, DKIM)"
}
# Update DNS records at registrar
update_dns_at_registrar() {
echo "Manual steps to update DNS:"
echo "1. Login to domain registrar"
echo "2. Navigate to DNS management"
echo "3. Update A record to point to VPS IP"
echo "4. Update MX records if using VPS mail"
echo "5. Add SPF, DKIM, DMARC records"
echo "6. Save changes"
echo "7. Wait for TTL expiration"
}
# Verify DNS propagation
check_dns_propagation() {
local domain=$1
echo "Checking DNS propagation for: $domain"
# Check various DNS servers
for nameserver in 8.8.8.8 1.1.1.1 208.67.222.222; do
echo ""
echo "Nameserver: $nameserver"
dig @$nameserver "$domain" A +short
dig @$nameserver "www.$domain" A +short
dig @$nameserver "$domain" MX +short
done
# Monitor propagation
echo ""
echo "Real-time propagation status:"
echo "https://www.whatsmydns.net/?d=$domain"
}
SSL Certificate Migration
Export and Import SSL Certificates
# Export SSL certificate from shared hosting
export_ssl_certificate() {
local shared_host=$1
local domain=$2
local cert_dir="/tmp/ssl-certs"
mkdir -p "$cert_dir"
echo "Exporting SSL certificate for: $domain"
# Export certificate, key, and intermediate
ssh "root@$shared_host" << EOF
# Find certificate files
find /etc/ssl -name "$domain*" -o -name "*$domain*" | while read file; do
cat "\$file"
done > /tmp/$domain-cert-bundle.pem
EOF
# Download certificate bundle
scp "root@$shared_host:/tmp/$domain-cert-bundle.pem" "$cert_dir/"
if [ $? -eq 0 ]; then
echo "✓ Certificate exported"
ls -lh "$cert_dir/$domain-cert-bundle.pem"
fi
}
# Import SSL certificate on VPS
import_ssl_certificate() {
local domain=$1
local cert_file=$2
local key_file=$3
echo "Importing SSL certificate for: $domain"
# Create certificate directory
mkdir -p "/etc/ssl/certs/$domain"
mkdir -p "/etc/ssl/private/$domain"
# Copy certificate
cp "$cert_file" "/etc/ssl/certs/$domain/certificate.crt"
cp "$key_file" "/etc/ssl/private/$domain/private.key"
# Set correct permissions
chmod 644 "/etc/ssl/certs/$domain/certificate.crt"
chmod 600 "/etc/ssl/private/$domain/private.key"
echo "✓ Certificate installed"
# Verify certificate
openssl x509 -in "/etc/ssl/certs/$domain/certificate.crt" -noout -text | grep -E "Subject:|Not After|Public Key"
}
# Use Let's Encrypt for free SSL
setup_lets_encrypt_ssl() {
local domain=$1
echo "Setting up Let's Encrypt SSL for: $domain"
# Install Certbot
apt-get install -y certbot python3-certbot-apache
# Generate certificate
certbot certonly --apache \
-d "$domain" \
-d "www.$domain" \
--agree-tos \
-m admin@"$domain" \
-n
if [ $? -eq 0 ]; then
echo "✓ Let's Encrypt certificate installed"
# Verify installation
certbot certificates
fi
}
Testing and Verification
Comprehensive Migration Testing
# Post-migration verification checklist
verify_migration_complete() {
local domain=$1
cat > /tmp/migration-verification.sh << 'EOF'
#!/bin/bash
DOMAIN=$1
VERIFICATION_LOG="/var/log/migration-verification.log"
verify_web_service() {
echo "Verifying web service..." | tee -a "$VERIFICATION_LOG"
for protocol in "http" "https"; do
status=$(curl -s -o /dev/null -w "%{http_code}" "$protocol://$DOMAIN")
if [ "$status" = "200" ]; then
echo "✓ $protocol://$DOMAIN responds with HTTP $status" | tee -a "$VERIFICATION_LOG"
else
echo "✗ $protocol://$DOMAIN responds with HTTP $status" | tee -a "$VERIFICATION_LOG"
fi
done
}
verify_database() {
echo "Verifying database..." | tee -a "$VERIFICATION_LOG"
mysql -u root -e "SHOW DATABASES;" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✓ MySQL is running and accessible" | tee -a "$VERIFICATION_LOG"
# Show database count
db_count=$(mysql -u root -sNe "SHOW DATABASES;" | wc -l)
echo "✓ Found $db_count databases" | tee -a "$VERIFICATION_LOG"
else
echo "✗ MySQL connection failed" | tee -a "$VERIFICATION_LOG"
fi
}
verify_email() {
echo "Verifying email..." | tee -a "$VERIFICATION_LOG"
# Check Postfix
systemctl is-active postfix > /dev/null && \
echo "✓ Postfix is running" | tee -a "$VERIFICATION_LOG" || \
echo "✗ Postfix is not running" | tee -a "$VERIFICATION_LOG"
# Check Dovecot
systemctl is-active dovecot > /dev/null && \
echo "✓ Dovecot is running" | tee -a "$VERIFICATION_LOG" || \
echo "✗ Dovecot is not running" | tee -a "$VERIFICATION_LOG"
# Test mail submission
telnet localhost 587 > /dev/null 2>&1 && \
echo "✓ SMTP submission port is accessible" | tee -a "$VERIFICATION_LOG"
}
verify_ssl() {
echo "Verifying SSL certificate..." | tee -a "$VERIFICATION_LOG"
cert_file="/etc/ssl/certs/domain/certificate.crt"
if [ -f "$cert_file" ]; then
echo "✓ SSL certificate found" | tee -a "$VERIFICATION_LOG"
# Check expiration
expiry=$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2)
echo "✓ Certificate expires: $expiry" | tee -a "$VERIFICATION_LOG"
else
echo "✗ SSL certificate not found" | tee -a "$VERIFICATION_LOG"
fi
}
verify_dns() {
echo "Verifying DNS..." | tee -a "$VERIFICATION_LOG"
# Check A record
a_record=$(dig +short "$DOMAIN")
[ -n "$a_record" ] && echo "✓ A record resolves to: $a_record" | tee -a "$VERIFICATION_LOG"
# Check MX record
mx_record=$(dig +short "$DOMAIN" MX)
[ -n "$mx_record" ] && echo "✓ MX record: $mx_record" | tee -a "$VERIFICATION_LOG"
}
# Run all verifications
verify_web_service
verify_database
verify_email
verify_ssl
verify_dns
echo ""
echo "Verification report saved to: $VERIFICATION_LOG"
EOF
chmod +x /tmp/migration-verification.sh
bash /tmp/migration-verification.sh "$domain"
}
# Monitor for issues post-migration
monitor_post_migration() {
local domain=$1
local check_interval=300 # 5 minutes
local check_duration=86400 # 24 hours
echo "Monitoring migration status..."
start_time=$(date +%s)
while true; do
current_time=$(date +%s)
elapsed=$((current_time - start_time))
if [ $elapsed -gt $check_duration ]; then
echo "✓ 24-hour monitoring period complete"
break
fi
# Check web service
http_code=$(curl -s -o /dev/null -w "%{http_code}" "http://$domain")
[ "$http_code" = "200" ] && echo "[OK] HTTP: $http_code" || echo "[ERROR] HTTP: $http_code"
# Check email
postfix_status=$(systemctl is-active postfix)
[ "$postfix_status" = "active" ] && echo "[OK] Mail: Active" || echo "[ERROR] Mail: Inactive"
# Check database
mysql -u root -e "SELECT 1" > /dev/null 2>&1
[ $? -eq 0 ] && echo "[OK] Database: Connected" || echo "[ERROR] Database: Failed"
sleep $check_interval
done
}
Conclusion
Successful shared hosting to VPS migration requires:
- Planning: Document all current setup details comprehensively
- Testing: Verify VPS setup before starting migration
- File Transfer: Use rsync for reliable, resumable transfers
- Database: Export, verify, and import carefully
- Email: Test delivery before pointing DNS
- DNS: Lower TTL, update carefully, monitor propagation
- SSL: Verify certificates work on new platform
- Monitoring: Watch for issues in first 24 hours
Maintain access to both systems until migration is fully verified. The biggest risks are DNS propagation delays and email delivery issues. Always keep backups and have a rollback plan ready.


