Skip to content

将应用程序迁移到另一台 Coolify 主机

Coolify 没有内置选项可以将应用程序从一台服务器迁移到另一台服务器。

您需要手动在新服务器上部署应用程序,并复制数据库和卷。本指南将逐步引导您完成这一过程。

注意

我们假设您已经在目标服务器上安装了 Coolify 并准备好迁移您的应用程序。

1. 了解数据持久化

使用 Coolify 时,应用程序数据存储在以下两个位置之一:

绑定挂载

  • 使用绑定挂载时,主机目录或文件被映射到容器中。
  • 对主机上的目录或文件所做的任何更改都会立即反映在容器内。
  • 要备份数据,只需将主机目录或文件复制到新服务器,并更新应用程序配置中的绑定挂载路径。

卷挂载

  • 使用卷挂载时,会创建一个 Docker 卷(通常由 Coolify 创建,但您也可以自己设置)并用于存储应用程序数据。
  • 卷存储在 Docker 的卷目录中,通常位于 /var/lib/docker/volumes/<VOLUME_NAME>
  • 您不能直接复制该目录,而是 Docker 提供了一种使用临时容器进行安全备份和恢复的方法。

注意

由于绑定挂载可以通过直接复制文件来简单迁移,本指南将主要关注卷备份。

2. 备份和恢复概述

Docker 推荐的卷迁移过程如下:

  1. 挂载您的卷到一个临时容器中。
  2. 归档卷的内容到一个 tarball 文件中。
  3. 复制 tarball 文件从容器到您的主机,然后删除临时容器。
  4. 传输 tarball 文件到新服务器。
  5. 创建一个新的卷在目标服务器上。
  6. 挂载传输的 tarball 文件到一个临时容器中。
  7. 提取归档到新卷中。

这一系列步骤确保了一致、安全的备份和恢复。下面,我们将提供现成可用的脚本和详细说明。


注意

以下步骤包含脚本,可帮助您轻松、交互式地备份、传输和恢复卷。

它们还包括检查以确保卷和备份存在,以防止错误,如恢复到错误的卷。

3. 备份卷

  1. SSH 连接到您的服务器,该服务器上有您的 Docker 卷。

  2. 创建一个名为 backup.sh 的脚本

    sh
    touch backup.sh && chmod +x backup.sh
  3. 在编辑器中打开 backup.sh 并粘贴以下内容

    sh
    #!/bin/bash
    
    # === 输入提示 ===
    # 提示输入 Docker 卷名称并设置变量
    read -p "[ 备份代理 ] [ 输入 ] 请输入要备份的 Docker 卷名称: " VOLUME_NAME
    
    # 通知用户设置的卷名称
    echo "[ 备份代理 ] [ 信息 ] 备份卷已设置为 $VOLUME_NAME"
    
    # 检查输入的卷是否存在
    if ! docker volume ls --quiet | grep -q "^$VOLUME_NAME$"; then
        echo "[ 备份代理 ] [ 错误 ] 卷 '$VOLUME_NAME' 不存在,中止备份。"
        echo "[ 备份代理 ] [ 错误 ] 备份失败!"
        exit 1  # 如果卷不存在则退出
    else
        echo "[ 备份代理 ] [ 信息 ] 卷 '$VOLUME_NAME' 存在,继续备份..."
    fi
    
    # 提示输入保存备份的目录
    read -p "[ 备份代理 ] [ 输入 ] 请输入保存备份的目录(可选:按回车使用 ./volume-backup): " BACKUP_DIR
    # 如果未输入目录,则默认为 './volume-backup'
    BACKUP_DIR=${BACKUP_DIR:-./volume-backup}
    
    # 通知用户备份位置
    echo "[ 备份代理 ] [ 信息 ] 备份位置已设置为 $BACKUP_DIR"
    
    # 根据卷名称设置备份文件名
    BACKUP_FILE="${VOLUME_NAME}-backup.tar.gz"
    
    # 通知用户备份文件名
    echo "[ 备份代理 ] [ 信息 ] 备份文件名已设置为 $BACKUP_FILE"
    
    # === 脚本开始 ===
    # 检查备份目录是否存在
    if [ -d "$BACKUP_DIR" ]; then
        echo "[ 备份代理 ] [ 信息 ] 目录 '$BACKUP_DIR' 已存在,跳过目录创建。"
    else
        echo "[ 备份代理 ] [ 信息 ] 目录 '$BACKUP_DIR' 不存在,正在创建目录。"
        # 创建备份目录,如果创建失败则退出
        mkdir -p "$BACKUP_DIR" || { 
            echo "[ 备份代理 ] [ 错误 ] 无法创建目录 '$BACKUP_DIR',中止备份。"
            echo "[ 备份代理 ] [ 错误 ] 备份失败!"
            exit 1
        }
    fi
    
    # 执行备份操作
    echo "[ 备份代理 ] [ 信息 ] 正在备份卷: $VOLUME_NAME$BACKUP_DIR/$BACKUP_FILE"
    
    # 运行 Docker 容器创建备份
    docker run --rm \
      -v "$VOLUME_NAME":/volume \
      -v "$(pwd)/$BACKUP_DIR":/backup \
      busybox \
      tar czf /backup/"$BACKUP_FILE" -C /volume . || { 
        # 如果备份失败,打印错误消息并退出
        echo "[ 备份代理 ] [ 错误 ] 备份过程失败,中止。"
        echo "[ 备份代理 ] [ 错误 ] 备份失败!"
        exit 1
    }
    
    # 如果一切成功,通知用户
    echo "[ 备份代理 ] [ 成功 ] 备份已完成!"
  4. 通过运行以下命令查找卷名称

    sh
    docker volume ls

    或者从 Coolify 的持久存储页面(见下图)。

  5. 停止您的应用程序以执行干净的备份。

  6. 运行脚本

    sh
    ./backup.sh
    • 当提示时,粘贴卷名称。
    • Enter接受默认备份目录 (./volume-backup),或输入自定义路径。
  7. 验证您现在有一个目录(例如,volume-backup)包含 <VOLUME_NAME>-backup.tar.gz 文件。


4. 将备份传输到新服务器

提示

如果您已经知道如何手动传输备份文件,可以直接跳到下一步。

  1. 创建第二个名为 transfer.sh 的脚本

    sh
    touch transfer.sh && chmod +x transfer.sh
  2. 在编辑器中打开 transfer.sh 并粘贴以下内容

    sh
    #!/bin/bash
    
    # =============== 配置变量 ===============
    SSH_PORT=22
    SSH_USER="root"
    SSH_IP="192.168.1.222"
    SSH_KEY="$HOME/.ssh/local-vm"
    SOURCE_PATH="./volume-backup"
    DESTINATION_PATH="/root/backups/volume-backup"
    MAX_RETRIES=3  # 最大密码尝试次数
    
    echo "[ 传输代理 ] [ 信息 ] 开始传输..."
    echo "[ 传输代理 ] [ 信息 ] 尝试 SSH 密钥: $SSH_KEY"
    echo "[ 传输代理 ] [ 信息 ] 从: $SOURCE_PATH 传输"
    echo "[ 传输代理 ] [ 信息 ] 传输到: $SSH_USER@$SSH_IP:$DESTINATION_PATH"
    
    # 如果 SSH 密钥文件不存在,回退到密码模式
    if [ ! -f "$SSH_KEY" ]; then
      echo "[ 传输代理 ] [ 警告 ] 未找到 SSH 密钥 '$SSH_KEY'。回退到密码身份验证。"
      SSH_KEY=""
    fi
    
    # 如果需要基于密码的身份验证,确保安装了 Expect
    if [ -z "$SSH_KEY" ] && ! command -v expect >/dev/null 2>&1; then
      echo "[ 传输代理 ] [ 错误 ] 密码身份验证需要 expect 包,但未安装(请使用 sudo apt install expect 手动安装后再试)。中止。"
      exit 1
    fi
    
    # ---------------------------------------------
    # 辅助函数:通过执行 "ssh … exit" 测试 $1(密码)是否有效
    # 如果有效返回 0,如果 "拒绝权限"(或任何其他失败)返回 1
    test_password_with_expect() {
      local PW="$1"
    
      expect -c "
        log_user 0
        set timeout 15
        spawn ssh -o StrictHostKeyChecking=no -p $SSH_PORT $SSH_USER@$SSH_IP exit
        expect {
          \"*?assword:\" {
            send -- \"$PW\r\"
            expect {
              \"Permission denied\" { exit 1 }
              eof { exit [lindex [wait] 3] }
            }
          }
          eof {
            exit [lindex [wait] 3]
          }
        }
      " >/dev/null 2>&1
    
      return $?
    }
    
    # 最多提示 $MAX_RETRIES 次输入正确密码
    get_password() {
      local retries=0
      while [ $retries -lt $MAX_RETRIES ]; do
        read -s -p "[ 传输代理 ] [ 输入 ] 请输入 $SSH_USER@$SSH_IP 的 SSH 密码: " SSHPASS
        echo ""
        test_password_with_expect "$SSHPASS"
        if [ $? -eq 0 ]; then
          # 密码正确
          return 0
        else
          echo "[ 传输代理 ] [ 错误 ] 无效密码。请重试。"
          retries=$((retries + 1))
        fi
      done
    
      echo "[ 传输代理 ] [ 错误 ] 已达到最大重试次数。中止。"
      exit 1
    }
    
    # ---------------------------------------------
    # 步骤 0:尝试 SSH 密钥身份验证(如果设置了密钥)
    if [ -n "$SSH_KEY" ]; then
      # 使用 BatchMode 防止回退到密码提示
      ssh -i "$SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=no -p "$SSH_PORT" \
        "$SSH_USER@$SSH_IP" exit >/dev/null 2>&1
      RC=$?
      if [ $RC -eq 0 ]; then
        echo "[ 传输代理 ] [ 信息 ] SSH 密钥有效!"
        USING_KEY=true
      else
        echo "[ 传输代理 ] [ 警告 ] SSH 密钥身份验证失败。回退到密码身份验证。"
        SSH_KEY=""
        USING_KEY=false
      fi
    else
      USING_KEY=false
    fi
    
    # 如果密钥身份验证失败(或没有密钥),提示输入密码
    if [ "$USING_KEY" = false ]; then
      get_password
      echo "[ 传输代理 ] [ 信息 ] 密码有效!"
    fi
    
    # ---------------------------------------------
    # 步骤 1:确保远程端存在完整的 DESTINATION_PATH。
    echo "[ 传输代理 ] [ 信息 ] 确保远程目录 '$DESTINATION_PATH' 存在..."
    if [ -n "$SSH_KEY" ]; then
      ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" \
        "$SSH_USER@$SSH_IP" "mkdir -p $DESTINATION_PATH" >/dev/null 2>&1
      MKRC=$?
    else
      expect -c "
        log_user 0
        set timeout 5
        spawn ssh -o StrictHostKeyChecking=no -p $SSH_PORT $SSH_USER@$SSH_IP mkdir -p $DESTINATION_PATH
        expect {
          \"*?assword:\" {
            send -- \"$SSHPASS\r\"
            exp_continue
          }
          eof {
            exit [lindex [wait] 3]
          }
        }
      " >/dev/null 2>&1
      MKRC=$?
    fi
    
    if [ $MKRC -ne 0 ]; then
      echo "[ 传输代理 ] [ 错误 ] 无法创建远程目录。中止。"
      exit 1
    fi
    
    # ---------------------------------------------
    # 步骤 2:仅将本地 "volume-backup" 的内容复制到该文件夹。
    echo "[ 传输代理 ] [ 信息 ] 开始文件传输..."
    
    # 将任何 SCP 错误输出捕获到临时文件,以便在出现问题时显示
    SCP_LOG="$(mktemp)"
    if [ -n "$SSH_KEY" ]; then
      # 抑制标准输出,仅捕获错误输出
      scp -i "$SSH_KEY" -o StrictHostKeyChecking=no -P "$SSH_PORT" -r \
          "$SOURCE_PATH"/. "$SSH_USER@$SSH_IP:$DESTINATION_PATH" > /dev/null 2> "$SCP_LOG"
      SCP_RC=$?
    else
      expect -c "
        log_user 0
        set timeout -1
        spawn scp -o StrictHostKeyChecking=no -P $SSH_PORT -r $SOURCE_PATH/. $SSH_USER@$SSH_IP:$DESTINATION_PATH
        expect {
          \"*?assword:\" {
            send -- \"$SSHPASS\r\"
            exp_continue
          }
          eof {
            exit [lindex [wait] 3]
          }
        }
      " 2> "$SCP_LOG"
      SCP_RC=$?
    fi
    
    if [ $SCP_RC -eq 0 ]; then
      echo "[ 传输代理 ] [ 成功 ] 传输已完成。"
      rm -f "$SCP_LOG"
      exit 0
    else
      echo "[ 传输代理 ] [ 错误 ] 传输失败。"
      while IFS= read -r line; do
        echo "[ 传输代理 ]    $line"
      done < "$SCP_LOG"
      rm -f "$SCP_LOG"
      exit 1
    fi
  3. 调整顶部的变量SSH_IPSSH_USERSSH_KEYDESTINATION_PATH)以匹配您的新服务器。

  4. 运行传输

    sh
    ./transfer.sh
    • 如果基于密钥的身份验证成功,备份文件夹将通过 SCP 复制。
    • 否则,系统会提示您输入 SSH 密码。

5. 在新服务器上恢复备份

注意

在这个示例中,我们将使用 Umami Analytics(PostgreSQL)来展示如何恢复数据库支持的应用程序。根据您自己的数据库调整路径和卷名。

  1. 在新服务器上部署您的应用程序,然后停止它,以便卷将被创建但不会被使用。

  2. SSH 连接到新服务器创建一个名为 restore.sh 的脚本

    sh
    touch restore.sh && chmod +x restore.sh
  3. 将以下内容粘贴到 restore.sh

    sh
    #!/bin/bash
    
    # === 目标卷名输入 ===
    # 提示输入要恢复到的目标 Docker 卷名
    read -p "[ 恢复代理 ] [ 输入 ] 请输入要恢复到的目标 Docker 卷名: " TARGET_VOLUME
    
    # === 卷检查 ===
    # 检查目标卷是否存在
    if ! docker volume ls --quiet | grep -q "^$TARGET_VOLUME$"; then
      echo "[ 恢复代理 ] [ 错误 ] 卷 '$TARGET_VOLUME' 不存在。"
    
      # 询问用户是否要创建卷
      read -p "[ 恢复代理 ] [ 输入 ] 您是否要创建一个名为 '$TARGET_VOLUME' 的新卷?(y/N): " create_volume
      if [[ "$create_volume" == "y" ]]; then
        echo "[ 恢复代理 ] [ 信息 ] 正在创建卷 '$TARGET_VOLUME'..."
        docker volume create "$TARGET_VOLUME" || { 
          echo "[ 恢复代理 ] [ 错误 ] 无法创建卷 '$TARGET_VOLUME',中止恢复。"
          echo "[ 恢复代理 ] [ 错误 ] 恢复失败!"
          exit 1
        }
        echo "[ 恢复代理 ] [ 信息 ] 卷 '$TARGET_VOLUME' 创建成功。"
      else
        echo "[ 恢复代理 ] [ 信息 ] 卷 '$TARGET_VOLUME' 不存在且用户选择不创建。中止恢复。"
        echo "[ 恢复代理 ] [ 错误 ] 恢复失败!"
        exit 1
      fi
    else
      echo "[ 恢复代理 ] [ 信息 ] 卷 '$TARGET_VOLUME' 存在,继续..."
    fi
    
    # === 备份目录输入 ===
    # 提示输入备份目录(默认:./volume-backup)
    read -p "[ 恢复代理 ] [ 输入 ] 请输入备份目录(默认:./volume-backup): " BACKUP_DIR
    BACKUP_DIR=${BACKUP_DIR:-./volume-backup}
    
    # === 备份目录检查 ===
    # 检查备份目录是否存在
    if [[ ! -d "$BACKUP_DIR" ]]; then
      echo "[ 恢复代理 ] [ 错误 ] 未找到备份目录: $BACKUP_DIR"
      echo "[ 恢复代理 ] [ 错误 ] 恢复失败!"
      exit 1
    fi
    echo "[ 恢复代理 ] [ 信息 ] 找到备份目录 '$BACKUP_DIR',继续..."
    
    # === 备份文件输入 ===
    # 提示输入备份文件名
    read -p "[ 恢复代理 ] [ 输入 ] 请输入备份文件名(例如:abc123_postgresql.tar.gz): " BACKUP_FILE
    
    # === 备份文件检查 ===
    # 检查备份文件是否存在
    if [[ ! -f "$BACKUP_DIR/$BACKUP_FILE" ]]; then
      echo "[ 恢复代理 ] [ 错误 ] 未找到备份文件: $BACKUP_DIR/$BACKUP_FILE"
      echo "[ 恢复代理 ] [ 错误 ] 恢复失败!"
      exit 1
    fi
    echo "[ 恢复代理 ] [ 信息 ] 找到备份文件 '$BACKUP_FILE',继续..."
    
    # === 安全确认 ===
    echo "[ 恢复代理 ] [ 信息 ] 请确保使用 '$TARGET_VOLUME' 的容器已停止!"
    read -p "[ 恢复代理 ] [ 输入 ] 继续恢复?(y/N): " confirm
    if [[ "$confirm" != "y" ]]; then
      echo "[ 恢复代理 ] [ 错误 ] 恢复失败:用户取消。"
      exit 1
    fi
    
    # === 恢复开始 ===
    # 通知用户恢复开始
    echo "[ 恢复代理 ] [ 信息 ] 正在将 $BACKUP_FILE 恢复到卷: $TARGET_VOLUME"
    
    # 运行 Docker 容器恢复备份
    docker run --rm \
      -v "$TARGET_VOLUME":/volume \
      -v "$(pwd)/$BACKUP_DIR":/backup \
      busybox \
      sh -c "cd /volume && tar xzf /backup/$BACKUP_FILE" || { 
        # 如果恢复过程失败,打印错误消息并退出
        echo "[ 恢复代理 ] [ 错误 ] Docker 恢复过程失败,中止。"
        echo "[ 恢复代理 ] [ 错误 ] 恢复失败!"
        exit 1
    }
    
    # 如果一切成功,通知用户
    echo "[ 恢复代理 ] [ 成功 ] 恢复已完成!"
  4. 运行脚本

    sh
    ./restore.sh
    • 输入卷名(来自 docker volume ls 命令或 Coolify 的持久存储页面)。
    • Enter接受 ./volume-backup,或输入自定义备份路径。
    • 输入备份文件名(例如:umami_postgresql-backup.tar.gz)。
    • 输入 y 确认您要继续。

6. 启动您的应用程序

恢复完成后,转到 Coolify 的仪表板并点击部署

您的应用程序现在应该使用迁移的数据。如果没有,或者日志显示错误,请重复恢复步骤以确保所有文件都正确复制。

注意

如果新服务器上的数据库凭据(用户名、数据库名称或密码)与旧服务器不同,请在 Coolify 的仪表板中更新它们以匹配旧服务器的凭据。