Quick and Easy Migration from FastPanel to CloudPanel: A Complete Guide

In today’s rapidly evolving web hosting landscape, choosing the right control panel can significantly impact your website management efficiency. CloudPanel has emerged as one of the most robust and user-friendly hosting panels available, offering superior performance and simplified management compared to traditional alternatives. This guide will walk you through a streamlined migration process from FastPanel to CloudPanel, complete with automation scripts for handling multiple domains.

Why Choose CloudPanel?

CloudPanel represents the next generation of hosting control panels, designed with modern web technologies in mind. Its lightweight architecture and optimized performance make it an ideal choice for both small websites and large-scale deployments. Unlike traditional panels, CloudPanel’s resource-efficient design ensures your servers maintain peak performance while providing an intuitive management interface. Moreover, its seamless integration with popular services like Cloudflare makes it a compelling choice for professional hosting setups.

Prerequisites

Before beginning the migration process, ensure you have the following:

  • Access to both FastPanel and CloudPanel servers
  • Cloudflare API credentials
  • Basic command-line knowledge
  • Domains already added to CloudPanel
  • Backup of all websites (always important before migration)

Step 0. Create backup of all your websites

This is my script to backup all websites in Fastpanel. I have a limited space avaible on server – so I moved the websites first less 100Mb. You can remove this limitation.

#!/bin/bash

# Go to the base directory
cd /var/www

# Create backup directory if it doesn't exist
backup_dir="/var/www/backups"
mkdir -p "$backup_dir"

# Size limit in bytes (100MB = 100*1024*1024)
size_limit=$((100*1024*1024))

# Initialize arrays
declare -a eligible_websites
declare -a archived_websites
declare -a failed_archives

# Function to create archives for eligible websites
create_archives() {
    echo "Creating archives..."
    echo "------------------"
    
    # Pattern 1: *_usr pattern
    for user_dir in *_usr; do
        if [ -d "$user_dir/data/www" ]; then
            process_directory "$user_dir"
        fi
    done

    # Pattern 2: direct username pattern
    for user_dir in *; do
        if [ -d "$user_dir/data/www" ] && [[ ! $user_dir =~ _usr$ ]]; then
            process_directory "$user_dir"
        fi
    done
}

# Function to process websites in a directory
process_directory() {
    local user_dir="$1"
    for website_dir in "$user_dir/data/www"/*; do
        if [ -d "$website_dir" ]; then
            website_name=$(basename "$website_dir")
            dir_size=$(du -sb "$website_dir" | cut -f1)
            
            if [ "$dir_size" -lt "$size_limit" ]; then
                echo "Processing $website_name ($(du -h "$website_dir" | cut -f1))"
                # Create archive
                if tar -czf "$backup_dir/${website_name}.tar.gz" -C "$user_dir/data/www" "$website_name"; then
                    eligible_websites+=("$website_name")
                    archived_websites+=("$website_name")
                    echo "✅ Successfully archived $website_name"
                else
                    failed_archives+=("$website_name")
                    echo "❌ Failed to archive $website_name"
                fi
            else
                echo "Skipping $website_name - too large ($(du -h "$website_dir" | cut -f1))"
            fi
        fi
    done
}

# Function to verify archives
verify_archives() {
    echo -e "\nVerifying archives..."
    echo "-------------------"
    
    cd "$backup_dir" || exit 1
    
    # Check each archive
    for website in "${archived_websites[@]}"; do
        if [ -f "${website}.tar.gz" ]; then
            # Test the archive integrity
            if tar -tf "${website}.tar.gz" &>/dev/null; then
                echo "✅ Verified: ${website}.tar.gz"
            else
                echo "❌ Corrupt archive: ${website}.tar.gz"
                failed_archives+=("$website")
            fi
        else
            echo "❌ Missing archive: ${website}.tar.gz"
            failed_archives+=("$website")
        fi
    done

    # Print summary
    echo -e "\nSummary:"
    echo "--------"
    echo "Total eligible websites (under 20MB): ${#eligible_websites[@]}"
    echo "Successfully archived: ${#archived_websites[@]}"
    echo "Archives location: $backup_dir"
    
    if [ ${#failed_archives[@]} -eq 0 ]; then
        echo "✅ All eligible websites were archived successfully!"
    else
        echo -e "\n❌ Failed or missing archives:"
        printf '%s\n' "${failed_archives[@]}"
    fi
}

# Run the functions
create_archives
verify_archives

Step 1: Setting Up SSL Certificates with Cloudflare

Managing SSL certificates for multiple domains can be time-consuming, but automation makes it efficient. The following script handles bulk certificate creation in Cloudflare and installation in CloudPanel:

#!/bin/bash
# Save as create-cloudflare-certs.sh

# Cloudflare credentials
CF_EMAIL="[email protected]"
CF_KEY="your-cloudflare-api-key"

# Function to cleanup temporary files
cleanup() {
    rm -f private.key certificate.pem domain.csr
}

# Function to generate and install certificate for a domain
process_domain() {
    local domain="$1"
    local cpanel_name="$2"
    
    echo "Processing domain: $domain (CloudPanel name: $cpanel_name)"
    
    # Cleanup any existing files
    cleanup
    
    # Generate private key and CSR
    echo "Generating private key and CSR for $domain..."
    openssl req -new -newkey rsa:2048 -nodes -keyout private.key -out domain.csr \
        -subj "/C=US/ST=State/L=City/O=Organization/CN=$domain" 2>/dev/null
    
    if [ ! -f domain.csr ]; then
        echo "Failed to generate CSR for $domain"
        return 1
    fi
    
    # Read and format CSR
    CSR=$(cat domain.csr | awk '{printf "%s\\n", $0}')
    
    # Create JSON payload
    JSON_DATA=$(cat << EOF
{
    "hostnames": ["$domain", "*.$domain"],
    "request_type": "origin-rsa",
    "requested_validity": 5475,
    "csr": "-----BEGIN CERTIFICATE REQUEST-----\\n${CSR}-----END CERTIFICATE REQUEST-----"
}
EOF
)
    
    # Request certificate from Cloudflare
    echo "Requesting certificate from Cloudflare for $domain..."
    RESPONSE=$(curl -s -X POST "https://api.cloudflare.com/client/v4/certificates" \
        -H "X-Auth-Email: $CF_EMAIL" \
        -H "X-Auth-Key: $CF_KEY" \
        -H "Content-Type: application/json" \
        -d "$JSON_DATA")
    
    if echo "$RESPONSE" | grep -q '"success":true'; then
        # Extract certificate
        echo "$RESPONSE" | jq -r '.result.certificate' > certificate.pem
        
        # Install certificate in CloudPanel
        echo "Installing certificate in CloudPanel for $domain..."
        clpctl site:install:certificate --domainName="$domain" \
            --privateKey="/root/private.key" \
            --certificate="/root/certificate.pem"
        
        local STATUS=$?
        if [ $STATUS -eq 0 ]; then
            echo "Successfully installed certificate for $domain"
            echo "----------------------------------------"
        else
            echo "Failed to install certificate in CloudPanel for $domain"
            echo "----------------------------------------"
            return 1
        fi
    else
        echo "Failed to get certificate from Cloudflare for $domain:"
        echo "$RESPONSE" | jq -r '.errors[].message'
        echo "----------------------------------------"
        return 1
    fi
}

# Main execution
echo "Starting bulk certificate installation..."
echo "----------------------------------------"

# Your domains list (array of domain and cpanel name pairs)
declare -A domains=(
    "domain.com"
    "domain2.com"
    #INSERT ALL OTHER YOUR DOMAINS.
)

# Counter for statistics
total=${#domains[@]}
success=0
failed=0

# Process each domain
for domain in "${domains[@]}"; do
    if process_domain "$domain" ""; then
        ((success++))
    else
        ((failed++))
        echo "Failed to process: $domain"
    fi
done

# Final cleanup
cleanup

# Print summary
echo "========== Summary =========="
echo "Total domains processed: $total"
echo "Successful: $success"
echo "Failed: $failed"
echo "==========================="

This script streamlines the SSL setup process by automatically generating certificates through Cloudflare’s Origin CA and installing them in CloudPanel, ensuring secure connections for all your websites.

Step 2: Configuring Cloudflare Settings

Proper Cloudflare configuration is crucial for optimal website performance and security. Our automation script applies recommended settings across all domains:

#!/bin/bash
# Save as update-cloudflare-settings.sh

# Cloudflare credentials
CF_EMAIL="[email protected]"
CF_KEY="your-cloudflare-api-key"

# Function to get zone ID for a domain
get_zone_id() {
    local domain="$1"
    local response
    
    response=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$domain" \
        -H "X-Auth-Email: $CF_EMAIL" \
        -H "X-Auth-Key: $CF_KEY" \
        -H "Content-Type: application/json")
    
    echo "$response" | jq -r '.result[0].id'
}

# Function to update a setting for a zone
update_setting() {
    local zone_id="$1"
    local setting_id="$2"
    local value="$3"
    
    local response
    response=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$zone_id/settings/$setting_id" \
        -H "X-Auth-Email: $CF_EMAIL" \
        -H "X-Auth-Key: $CF_KEY" \
        -H "Content-Type: application/json" \
        -d "{\"value\": \"$value\"}")
    
    if echo "$response" | jq -e '.success' > /dev/null; then
        echo "Successfully updated $setting_id to $value"
    else
        echo "Failed to update $setting_id:"
        echo "$response" | jq -r '.errors[].message'
    fi
}

# Settings to update
declare -A settings=(
    ["always_online"]="on"
    ["always_use_https"]="on"
    ["automatic_https_rewrites"]="on"
    ["brotli"]="on"
    ["browser_check"]="on"
    ["http2"]="on"
    ["http3"]="on"
    ["security_level"]="low"
    ["ssl"]="full"
)

# List of domains
declare -A domains=(
    ["domain.com"]="domainuser"
    #INSERT ALL OTHER YOUR DOMAINS.
    #"domainuser" - this is optional Cloudpanel username
)

# Counter for statistics
total=${#domains[@]}
success=0
failed=0

# Process each domain
for domain in "${!domains[@]}"; do
    echo "Processing domain: $domain"
    
    # Get zone ID
    zone_id=$(get_zone_id "$domain")
    
    if [ -z "$zone_id" ] || [ "$zone_id" = "null" ]; then
        echo "Failed to get zone ID for $domain"
        ((failed++))
        continue
    fi
    
    echo "Zone ID: $zone_id"
    
    # Update each setting
    settings_success=true
    for setting in "${!settings[@]}"; do
        echo "Updating $setting to ${settings[$setting]}"
        update_setting "$zone_id" "$setting" "${settings[$setting]}"
        if [ $? -ne 0 ]; then
            settings_success=false
        fi
    done
    
    if $settings_success; then
        ((success++))
        echo "Successfully updated all settings for $domain"
    else
        ((failed++))
        echo "Some settings failed for $domain"
    fi
    
    echo "----------------------------------------"
done

# Print summary
echo "========== Summary =========="
echo "Total domains processed: $total"
echo "Successful: $success"
echo "Failed: $failed"
echo "==========================="

These optimized settings ensure your websites benefit from Cloudflare’s full security and performance features, including proper SSL configuration and caching policies.

Step 3: DNS Records Management

Updating DNS records is a critical step in the migration process. This script handles bulk DNS updates in Cloudflare:

#!/bin/bash
# Save as update-cloudflare-dns.sh

# Cloudflare credentials
CF_EMAIL="[email protected]"
CF_KEY="your-cloudflare-api-key"

# Function to get zone ID for a domain
get_zone_id() {
    local domain="$1"
    local response
    
    response=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones?name=$domain" \
        -H "X-Auth-Email: $CF_EMAIL" \
        -H "X-Auth-Key: $CF_KEY" \
        -H "Content-Type: application/json")
    
    echo "$response" | jq -r '.result[0].id'
}

# Function to update a setting for a zone
update_setting() {
    local zone_id="$1"
    local setting_id="$2"
    local value="$3"
    
    local response
    response=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$zone_id/settings/$setting_id" \
        -H "X-Auth-Email: $CF_EMAIL" \
        -H "X-Auth-Key: $CF_KEY" \
        -H "Content-Type: application/json" \
        -d "{\"value\": \"$value\"}")
    
    if echo "$response" | jq -e '.success' > /dev/null; then
        echo "Successfully updated $setting_id to $value"
    else
        echo "Failed to update $setting_id:"
        echo "$response" | jq -r '.errors[].message'
    fi
}

# Settings to update
declare -A settings=(
    ["always_online"]="on"
    ["always_use_https"]="on"
    ["automatic_https_rewrites"]="on"
    ["brotli"]="on"
    ["browser_check"]="on"
    ["http2"]="on"
    ["http3"]="on"
    ["security_level"]="low"
    ["ssl"]="strict"
)

# List of domains
declare -A domains=(
    ["domain.com"]="domainuser"
    #INSERT ALL OTHER YOUR DOMAINS.
    #"domainuser" - this is optional Cloudpanel username
)

# Counter for statistics
total=${#domains[@]}
success=0
failed=0

# Process each domain
for domain in "${!domains[@]}"; do
    echo "Processing domain: $domain"
    
    # Get zone ID
    zone_id=$(get_zone_id "$domain")
    
    if [ -z "$zone_id" ] || [ "$zone_id" = "null" ]; then
        echo "Failed to get zone ID for $domain"
        ((failed++))
        continue
    fi
    
    echo "Zone ID: $zone_id"
    
    # Update each setting
    settings_success=true
    for setting in "${!settings[@]}"; do
        echo "Updating $setting to ${settings[$setting]}"
        update_setting "$zone_id" "$setting" "${settings[$setting]}"
        if [ $? -ne 0 ]; then
            settings_success=false
        fi
    done
    
    if $settings_success; then
        ((success++))
        echo "Successfully updated all settings for $domain"
    else
        ((failed++))
        echo "Some settings failed for $domain"
    fi
    
    echo "----------------------------------------"
done

# Print summary
echo "========== Summary =========="
echo "Total domains processed: $total"
echo "Successful: $success"
echo "Failed: $failed"
echo "==========================="

This automation ensures all domains correctly point to your new CloudPanel server while maintaining any existing DNS records like MX or CNAME entries.

Step 4: Cleaning Up FastPanel

The final step involves removing websites from your old FastPanel installation:

#!/bin/bash

# Create log file with timestamp
LOG_FILE="site_deletion_$(date +%Y%m%d_%H%M%S).log"

# Function to log messages to both console and file
log_message() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# Function to delete a site
delete_site() {
    local id="$1"
    local domain="$2"
    
    log_message "Attempting to delete site: $domain (ID: $id)"
    
    # Execute deletion command
    if mogwai sites delete --id="$id"; then
        log_message "Successfully deleted site: $domain (ID: $id)"
        return 0
    else
        log_message "Failed to delete site: $domain (ID: $id)"
        return 1
    fi
}

# Initialize counters
total=0
success=0
failed=0

# Array of sites to delete (ID|domain format)
declare -a SITES=(
"999|domain.com"
)

# Print header
log_message "Starting site deletion process"
log_message "Total sites to process: ${#SITES[@]}"
log_message "----------------------------------------"

# Process each site
for site in "${SITES[@]}"; do
    IFS='|' read -r id domain <<< "$site"
    ((total++))
    
    # Print progress
    log_message "Processing $total/${#SITES[@]}: $domain (ID: $id)"
    
    # Delete site
    if delete_site "$id" "$domain"; then
        ((success++))
    else
        ((failed++))
        echo "$id|$domain" >> failed_deletions.txt
    fi
    
    # Add a small delay to prevent overloading
    sleep 1
done

# Print summary
log_message "========== Summary =========="
log_message "Total sites processed: $total"
log_message "Successfully deleted: $success"
log_message "Failed to delete: $failed"
log_message "==========================="

# If there were failures, notify about the log file
if [ $failed -gt 0 ]; then
    log_message "Failed deletions have been logged to: failed_deletions.txt"
fi

This script helps you cleanly remove websites from FastPanel after confirming successful migration to CloudPanel.

——

Migrating from FastPanel to CloudPanel doesn’t have to be a daunting task. With these automation scripts and proper planning, you can efficiently transfer multiple websites while ensuring proper configuration and security. CloudPanel’s modern architecture and feature set make it an excellent choice for contemporary web hosting needs, providing a solid foundation for your web projects.

Remember to always backup your data before migration and test these scripts with a few domains before running them on your entire domain portfolio. The investment in moving to CloudPanel will pay off through improved performance, easier management, and a more modern hosting environment.

Additional Resources

Note: Remember to replace placeholder credentials and customize script parameters according to your specific needs.