Skip to content

Instantly share code, notes, and snippets.

@Geczy
Last active June 6, 2025 15:36
Show Gist options
  • Save Geczy/83c1c77389be94ed4709fc283a0d7e23 to your computer and use it in GitHub Desktop.
Save Geczy/83c1c77389be94ed4709fc283a0d7e23 to your computer and use it in GitHub Desktop.
Migrate Coolify to a new server
@arxkdev
Copy link

arxkdev commented Jan 8, 2025

Here's a modified version of @AspireOne's ssh-agent version. It fixes the volumes being separated wrong when extracting the volumes from a container.

#!/bin/bash

# This script will backup your Coolify instance and move everything to a new server. Docker volumes, Coolify database, and ssh keys

# 1. Script must run on the source server
# 2. Have all the containers running that you want to migrate

# Configuration - Modify as needed
sshKeyPath="$HOME/.ssh/your_private_key" # Key to destination server
destinationHost="server.example.com" # destination server IP or domain

# -- Shouldn't need to modify anything below --
backupSourceDir="/data/coolify/"
backupFileName="coolify_backup.tar.gz"

# Function to initialize ssh-agent and add the SSH key
initialize_ssh_agent() {
  # Check if ssh-agent is already running
  if [ -z "$SSH_AGENT_PID" ] || ! ps -p "$SSH_AGENT_PID" > /dev/null 2>&1; then
    echo "🔄 Starting ssh-agent..."
    eval "$(ssh-agent -s)"
    if [ $? -ne 0 ]; then
      echo "❌ Failed to start ssh-agent"
      exit 1
    fi
    echo "✅ ssh-agent started"
  else
    echo "✅ ssh-agent is already running"
  fi

  # Add the SSH key to the agent
  echo "🔒 Adding SSH key to ssh-agent"
  ssh-add "$sshKeyPath"
  if [ $? -ne 0 ]; then
    echo "❌ Failed to add SSH key. Please ensure the passphrase is correct."
    exit 1
  fi
  echo "✅ SSH key added to ssh-agent"
}

# Initialize ssh-agent and add the SSH key
initialize_ssh_agent

# Check if the source directory exists
if [ ! -d "$backupSourceDir" ]; then
  echo "❌ Source directory $backupSourceDir does not exist"
  exit 1
fi
echo "✅ Source directory exists"

# Check if the SSH key file exists
if [ ! -f "$sshKeyPath" ]; then
  echo "❌ SSH key file $sshKeyPath does not exist"
  exit 1
fi
echo "✅ SSH key file exists"

# Check if we can SSH to the destination server, ignore "The authenticity of host can't be established." errors
if ! ssh -o "StrictHostKeyChecking no" -o "ConnectTimeout=5" root@"$destinationHost" "exit"; then
  echo "❌ SSH connection to $destinationHost failed"
  exit 1
fi
echo "✅ SSH connection successful"

# Get the names of all running Docker containers
containerNames=$(docker ps --format '{{.Names}}')

# Initialize an empty string to hold the volume paths
volumePaths=""

for containerName in $containerNames; do
  # Use a delimiter to separate the volume names
  volumeNames=$(docker inspect --format '{{range .Mounts}}{{.Name}}{{print "\n"}}{{end}}' "$containerName")

  # Now, we process each line (volume name) from the output
  while IFS= read -r volumeName; do
    # Check if the volumeName is not empty
    if [ -n "$volumeName" ]; then
      echo "Adding path: /var/lib/docker/volumes/$volumeName"
      volumePaths="$volumePaths /var/lib/docker/volumes/$volumeName"
    fi
  done <<< "$volumeNames"
done

echo "Final volumePaths: $volumePaths"
# Calculate the total size of the volumes
# shellcheck disable=SC2086
totalSize=$(du -csh $volumePaths 2>/dev/null | grep total | awk '{print $1}')

# Print the total size of the volumes
echo "✅ Total size of volumes to migrate: $totalSize"

# Print size of backupSourceDir
backupSourceDirSize=$(du -csh "$backupSourceDir" 2>/dev/null | grep total | awk '{print $1}')
echo "✅ Size of the source directory: $backupSourceDirSize"

# Check if the backup file already exists
if [ ! -f "$backupFileName" ]; then
  echo "🚸 Backup file does not exist, creating"

  # Recommend stopping docker before creating the backup
  echo "🚸 It's recommended to stop all Docker containers before creating the backup"
  read -rp "Do you want to stop Docker? (y/n): " answer
  if [[ "$answer" =~ ^[Yy]$ ]]; then
    if ! systemctl stop docker; then
      echo "❌ Docker stop failed"
      exit 1
    fi
    echo "✅ Docker stopped"
  else
    echo "🚸 Docker not stopped, continuing with the backup"
  fi

  # shellcheck disable=SC2086
  if ! tar --exclude='*.sock' -Pczf "$backupFileName" -C / "$backupSourceDir" "$HOME/.ssh/authorized_keys" $volumePaths; then
    echo "❌ Backup file creation failed"
    exit 1
  fi
  echo "✅ Backup file created"
else
  echo "🚸 Backup file already exists, skipping creation"
fi

# Define the remote commands to be executed
remoteCommands="
  # Check if Docker is a service
  if systemctl is-active --quiet docker; then
    # Stop Docker if it's a service
    if ! systemctl stop docker; then
      echo '❌ Docker stop failed';
      exit 1;
    fi
    echo '✅ Docker stopped';
  else
    echo 'ℹ️ Docker is not a service, skipping stop command';
  fi

  echo '🚸 Saving existing authorized keys...';
  cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys_backup;

  echo '🚸 Extracting backup file...';
  if ! tar -Pxzf - -C /; then
    echo '❌ Backup file extraction failed';
    exit 1;
  fi
  echo '✅ Backup file extracted';

  echo '🚸 Merging authorized keys...';
  cat ~/.ssh/authorized_keys_backup ~/.ssh/authorized_keys | sort | uniq > ~/.ssh/authorized_keys_temp;
  mv ~/.ssh/authorized_keys_temp ~/.ssh/authorized_keys;
  chmod 600 ~/.ssh/authorized_keys;
  echo '✅ Authorized keys merged';

  if ! curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash; then
    echo '❌ Coolify installation failed';
    exit 1;
  fi
  echo '✅ Coolify installed';
"

# SSH to the destination server, execute the remote commands
if ! ssh root@"$destinationHost" "$remoteCommands" < "$backupFileName"; then
  echo "❌ Remote commands execution or Docker restart failed"
  exit 1
fi
echo "✅ Remote commands executed successfully"

# Clean up - Ask the user for confirmation before removing the local backup file
echo "Do you want to remove the local backup file? (y/n)"
read -r answer
if [[ "$answer" =~ ^[Yy]$ ]]; then
  if ! rm -f "$backupFileName"; then
    echo "❌ Failed to remove local backup file"
    exit 1
  fi
  echo "✅ Local backup file removed"
else
  echo "🚸 Local backup file not removed"
fi

# Kill ssh-agent if it was started by this script
if [ -n "$SSH_AGENT_PID" ]; then
  echo "🔒 Stopping ssh-agent..."
  eval "$(ssh-agent -k)"
  echo "✅ ssh-agent stopped"
fi

@CatalanCabbage
Copy link

CatalanCabbage commented Jan 8, 2025

At this point we're ironically pummeling this with unmanaged versions on a version control platform :P
Time to make this a repo and accept PRs? I could do it but it's only right that you do it @Geczy considering you took the initiative :)

@Geczy
Copy link
Author

Geczy commented Jan 8, 2025

Good idea, I've made the repo here! https://github.com/Geczy/coolify-migration

Also updated this gist with a readme linking to the above

Thanks everyone for your contributions! Feel free to open a PR to manage these wonderful changes that are being suggested.

@siavashh
Copy link

Just wanted to thanks for this perfect script.

@dwillitzer
Copy link

dwillitzer commented Feb 13, 2025

Great script, thanks.

I created a coolify db restore, if backing up locally.

Long term we need a baseline CLI similar to Plesk provides for installation, updates, and recovery.

@nnethery
Copy link

nnethery commented Apr 8, 2025

Also reporting that the original script works perfectly with the expected setup on v4.0.0-beta.406

@mannerism
Copy link

mannerism commented May 16, 2025

I see that this script allows us to migrate Coolify instance, but how are you guys handling application migrations? For instance, running the script won't migrate the application such as Ghost CMS with underlying data to a new server?

@eltonciatto
Copy link

I see that this script allows us to migrate Coolify instance, but how are you guys handling application migrations? For instance, running the script won't migrate the application such as Ghost CMS with underlying data to a new server?

Yes. The script also copies the volumes of each application (e.g. Ghost CMS and its database) to the new server. After running it, simply go to the Coolify dashboard on the destination and “Redeploy” each app. This will cause Traefik/Compose to create new containers for the applications by mounting the migrated volumes, preserving all the content (posts, themes, databases, uploads, etc.).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment