soarli

Docker 版 GitLab 自动化高安全备份实战:无面板、强加密、直推 WebDAV
对于开发者和团队来说,GitLab 中的代码和 issue 就是核心资产。很多人在使用 Docker 部署 Git...
扫描右侧二维码阅读全文
26
2026/02

Docker 版 GitLab 自动化高安全备份实战:无面板、强加密、直推 WebDAV

对于开发者和团队来说,GitLab 中的代码和 issue 就是核心资产。很多人在使用 Docker 部署 GitLab 时,往往只配置了数据卷映射,却忽略了灾备问题。

今天分享一套基于 Shell 脚本的完整 GitLab 备份方案。这套方案无需依赖宝塔面板,不仅能自动打包数据库和代码仓库,还能备份至关重要的核心配置文件,并通过 7z 强加密后直推至支持 WebDAV 的云盘(如 123云盘、阿里云盘等),最后配合 Cron 实现每天凌晨自动执行。

一、 为什么不直接用官方的 backup 命令?

官方的 gitlab-backup create 命令确实好用,但它存在两个致命盲区:

  1. 不备份配置文件:它只会备份数据(代码、附件、数据库等)。如果你丢失了 /etc/gitlab/gitlab-secrets.json 文件,恢复备份后,所有开启双因素认证(2FA)的用户将无法登录,CI/CD 变量也会全部失效!
  2. 缺乏远端流转和加密能力:备份包默认只停留在本地服务器,且未经过加密。一旦服务器遭遇勒索病毒或硬件损坏,本地备份将形同虚设。

二、 本套方案的核心亮点

  • 全维度备份:自动打包官方 tar 数据包,并同时提取 gitlab.rbgitlab-secrets.json
  • 机器绑定,固定密码:脚本会读取服务器的硬件 ID 计算出固定的 32 位安全密码进行 7z 深度加密(隐藏文件名)。同一台机器每次运行密码相同,不同机器密码完全隔离,既安全又防遗忘。
  • 纯净日志输出:剔除了所有特殊符号,确保在任何老旧终端或日志采集中不会出现乱码。
  • 自动清理机制:自带本地临时目录防残留清理功能,并可按需开启云端过期备份清理(例如自动删除 365 天前的旧文件)。

三、 准备工作

在开始之前,我们需要确保宿主机上安装了 p7zip,用于提供高强度的 AES-256 加密。

Ubuntu / Debian 系统:

sudo apt update && sudo apt install p7zip-full -y

CentOS / RHEL 系统:

sudo yum install epel-release -y
sudo yum install p7zip p7zip-plugins -y

四、 核心备份脚本

在服务器的 /root 目录下新建文件 gitlab_backup.sh,将以下代码粘贴进去。请务必修改必填配置区域中的容器名称、映射路径和云盘信息。

#!/bin/bash

# ================= 必填配置区域 =================
# 1. 你的 GitLab 容器名称 (默认为 gitlab)
GITLAB_CONTAINER_NAME="gitlab"

# 2. GitLab 宿主机映射路径 (请根据你的 docker-compose.yml 修改)
GITLAB_BACKUP_DIR="/srv/gitlab/data/backups" # 数据目录下的 backups 文件夹
GITLAB_CONFIG_DIR="/srv/gitlab/config"       # 配置目录

# 3. 云盘 WebDAV 配置
WEBDAV_URL="https://webdav.123pan.cn/webdav"
WEBDAV_USER="你的WebDAV账号"
WEBDAV_PASS="你的WebDAV密码"

# 4. 机器专属的二级目录名称
WEBDAV_ENDPOINT="gitlab_server"

# 5. 自动清理配置
ENABLE_REMOTE_CLEAN="false" # 开启远端清理 (true/false)
REMOTE_CLEAN_DAYS="365"     # 保留多少天的备份
# ================================================

# 默认路径配置
TEMP_DIR="/tmp/gitlab_backup_encrypt"
DATE_STR=$(date +"%Y%m%d_%H%M")

# --- 1. 本地异常残留清理 ---
if [ -d "$TEMP_DIR" ]; then
    echo "[INFO] 正在检测并清理本地可能残留的临时文件..."
    rm -rf "${TEMP_DIR:?}"/*
fi
mkdir -p "$TEMP_DIR"

# --- 2. 密码生成机制 ---
if [ -f /etc/machine-id ]; then
    MACHINE_UUID=$(cat /etc/machine-id)
elif [ -f /sys/class/dmi/id/product_uuid ]; then
    MACHINE_UUID=$(cat /sys/class/dmi/id/product_uuid)
else
    MACHINE_UUID=$(hostname)
fi
ZIP_PASS=$(echo -n "${MACHINE_UUID}_GitLabSalt_2026" | sha256sum | awk '{print $1}' | cut -c 1-32)

echo "=================================================="
echo "[START] 开始执行: GitLab 数据+配置 深度加密直传"
echo "=================================================="
echo "[重要提示] 本机专属备份加密密码如下,请务必妥善保存:"
echo "[解压密码] ${ZIP_PASS}"
echo "=================================================="

# --- 3. 计算远端清理的阈值日期 ---
if [ "$ENABLE_REMOTE_CLEAN" == "true" ]; then
    THRESHOLD_DATE=$(date -d "-${REMOTE_CLEAN_DAYS} days" +%Y%m%d 2>/dev/null)
    if [ -z "$THRESHOLD_DATE" ]; then
        THRESHOLD_DATE=$(date -v-${REMOTE_CLEAN_DAYS}d +%Y%m%d 2>/dev/null)
    fi
fi

# --- 函数:静默创建远程 WebDAV 目录 ---
make_webdav_dir() {
    local DIR_URL=$1
    curl -s -X MKCOL -u "${WEBDAV_USER}:${WEBDAV_PASS}" "${DIR_URL}" > /dev/null 2>&1
}

# --- 函数:清理远端过期文件 ---
clean_old_webdav_files() {
    local DIR_URL=$1
    if [ "$ENABLE_REMOTE_CLEAN" != "true" ] || [ -z "$THRESHOLD_DATE" ]; then return; fi

    local HREFS=$(curl -s -X PROPFIND -u "${WEBDAV_USER}:${WEBDAV_PASS}" "${DIR_URL}/" -H "Depth: 1" 2>/dev/null | awk -F'[><]' '/:href>|href>/{for(i=1;i<=NF;i++) if($i~"href$") print $(i+1)}')
    
    for HREF in $HREFS; do
        local FILE_NAME=$(basename "$HREF")
        if [ "$FILE_NAME" == "$(basename "$DIR_URL")" ] || [ -z "$FILE_NAME" ]; then continue; fi
        local FILE_DATE=$(echo "$FILE_NAME" | grep -Eo '[0-9]{8}_[0-9]{4}' | cut -d'_' -f1)

        if [ -n "$FILE_DATE" ] && [ "$FILE_DATE" -lt "$THRESHOLD_DATE" ]; then
            echo "    -> [DELETE] 发现过期云端文件 (${FILE_DATE}),正在销毁: ${FILE_NAME}"
            local DOMAIN=$(echo "$WEBDAV_URL" | awk -F/ '{print $1"//"$3}')
            local DELETE_URL="${DOMAIN}${HREF}"
            if [[ "$HREF" == http* ]]; then DELETE_URL="$HREF"; fi
            curl -s -X DELETE -u "${WEBDAV_USER}:${WEBDAV_PASS}" "${DELETE_URL}" > /dev/null 2>&1
        fi
    done
}

# --- 函数:上传并自动触发清理 ---
upload_and_clean() {
    local LOCAL_FILE=$1
    local TARGET_REMOTE_DIR=$2
    local FILE_NAME=$(basename "$LOCAL_FILE")
    local REMOTE_URL="${TARGET_REMOTE_DIR}/${FILE_NAME}"

    echo "  -> [UPLOAD] 正在上传至云端: ${TARGET_REMOTE_DIR} ..."
    HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -u "${WEBDAV_USER}:${WEBDAV_PASS}" -T "${LOCAL_FILE}" "${REMOTE_URL}")

    if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 201 ] || [ "$HTTP_CODE" -eq 204 ]; then
        echo "  -> [SUCCESS] 上传成功!销毁本地缓存..."
        rm -f "$LOCAL_FILE"
        clean_old_webdav_files "${TARGET_REMOTE_DIR}"
    else
        echo "  -> [ERROR] 上传失败!(状态码: $HTTP_CODE) 已保留本地缓存防丢。"
    fi
}

echo "[INFO] [0/3] 正在云端构建基础分级目录..."
REMOTE_ROOT="${WEBDAV_URL}/${WEBDAV_ENDPOINT}"
REMOTE_DATA_ROOT="${REMOTE_ROOT}/Data"
REMOTE_CONF_ROOT="${REMOTE_ROOT}/Config"

make_webdav_dir "${REMOTE_ROOT}"
make_webdav_dir "${REMOTE_DATA_ROOT}"
make_webdav_dir "${REMOTE_CONF_ROOT}"

# 1. 触发 GitLab 内部数据备份
echo "--------------------------------------------------"
echo "[INFO] [1/3] 正在通过 Docker 触发 GitLab 数据备份..."
docker exec -t "$GITLAB_CONTAINER_NAME" gitlab-backup create > /dev/null 2>&1

LATEST_TAR=$(ls -t "${GITLAB_BACKUP_DIR}"/*_gitlab_backup.tar 2>/dev/null | head -n 1)
if [ -z "$LATEST_TAR" ]; then
    echo "[ERROR] 未在 ${GITLAB_BACKUP_DIR} 找到备份包!"
    exit 1
fi

# 2. 加密并上传 GitLab 数据备份
echo "--------------------------------------------------"
echo "[INFO] [2/3] 正在加密并上传 GitLab 数据包..."
DATA_ZIP="$TEMP_DIR/GitLab_Data_${DATE_STR}.7z"
7z a -p"${ZIP_PASS}" -mhe=on "${DATA_ZIP}" "${LATEST_TAR}" > /dev/null 2>&1
upload_and_clean "$DATA_ZIP" "${REMOTE_DATA_ROOT}"

# 3. 加密并上传 GitLab 核心配置文件
echo "--------------------------------------------------"
echo "[INFO] [3/3] 正在加密并上传 GitLab 核心配置文件..."
if [ -d "$GITLAB_CONFIG_DIR" ]; then
    CONF_ZIP="$TEMP_DIR/GitLab_Config_${DATE_STR}.7z"
    7z a -p"${ZIP_PASS}" -mhe=on "${CONF_ZIP}" "${GITLAB_CONFIG_DIR}/gitlab.rb" "${GITLAB_CONFIG_DIR}/gitlab-secrets.json" > /dev/null 2>&1
    upload_and_clean "$CONF_ZIP" "${REMOTE_CONF_ROOT}"
else
    echo "  -> [ERROR] 未找到配置目录 ${GITLAB_CONFIG_DIR},跳过!"
fi

echo "=================================================="
echo "[FINISH] GitLab 数据及配置备份全部完成!"
echo "[提醒] 再次提醒,本服务器解压密码为: ${ZIP_PASS}"
echo "=================================================="

保存后,赋予执行权限:

sudo chmod +x /root/gitlab_backup.sh

五、 定时任务踩坑指南(解决无权限报错)

既然需要自动备份,就不可避免要使用 cron。很多人手动运行脚本成功,但放入定时任务后却因为 Docker 执行权限问题导致备份失败。

核心避坑点:永远不要在 crontab 中写 sudo!后台非交互式执行时等不到你输入密码。

正确的做法是直接编辑系统全局定时任务,指定使用 root 用户运行。

执行:

sudo vi /etc/crontab

在文件最下方添加以下内容(注意时间后面的 root 字样):

30 4 * * * root /bin/bash /root/gitlab_backup.sh >> /var/log/gitlab_backup.log 2>&1

保存退出。这样配置后,系统将在每天凌晨 4:30 以最高权限执行备份,并将纯净的日志输出保存到 /var/log/gitlab_backup.log。以后如果忘记了 32 位解压密码,直接去日志里 cat 一下就能找回。

最后修改:2026 年 02 月 26 日 01 : 29 PM

发表评论