强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 9 章 - 镜像与备份

第 9 章 - 镜像与备份

确保代码数据的安全性和可用性是 Git 服务器运维的核心职责。本章介绍仓库镜像同步和完善的备份恢复策略。

9.1 镜像同步概述

9.1.1 镜像方向

方向说明用途
Push Mirror自推送到远程(如 GitHub)开源项目发布、多平台托管
Pull Mirror从远程拉取到本地引入开源项目、备份远程仓库
双向同步推送 + 拉取(需注意冲突)多站点部署

9.1.2 镜像方案对比

方案适用平台实时性复杂度
平台内置镜像Gitea/Forgejo/GitLab定时(分钟级)
Git 远程推送任意手动/定时
Webhook 触发任意近实时
第三方工具任意近实时

9.2 推送到 GitHub

9.2.1 Gitea/Forgejo 推送镜像

Web 界面配置

  1. 进入仓库 → Settings → Mirror Settings
  2. 选择 Push Mirror
  3. 填写目标 URL 和认证信息

API 配置

# 创建推送镜像
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
  -H "Content-Type: application/json" \
  "$GITEA_URL/api/v1/repos/owner/repo/mirror-sync" \
  -d '{
    "remote_name": "github",
    "remote_address": "https://github.com/your-org/repo.git",
    "username": "your-github-username",
    "password": "ghp_xxxxxxxxxxxx",
    "interval": "8h",
    "sync_on_push": true
  }'

9.2.2 Git 原生推送镜像

适用于任何 Git 服务器,通过远程仓库配置实现:

# 进入裸仓库目录
cd /opt/git/project.git

# 添加 GitHub 作为镜像远程
git remote add --mirror=push github https://github.com/your-org/project.git

# 设置认证(使用 credential store)
git config credential.helper store
echo "https://your-github-username:[email protected]" >> ~/.git-credentials
chmod 600 ~/.git-credentials

# 手动同步
git push github

# 验证
git remote -v
# github  https://github.com/your-org/project.git (push)
# origin  /opt/git/project.git (push)

9.2.3 批量镜像推送脚本

#!/bin/bash
# batch-mirror-push.sh - 批量推送到 GitHub

GIT_ROOT="/opt/git"
GITHUB_ORG="your-github-org"
GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
LOG_FILE="/var/log/git-mirror.log"

log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"
}

# 获取所有仓库
find "$GIT_ROOT" -name "*.git" -type d | sort | while read repo; do
    repo_name=$(basename "$repo" .git)
    
    log "Mirroring: $repo_name"
    
    # 检查 GitHub 是否存在该仓库
    http_code=$(curl -s -o /dev/null -w "%{http_code}" \
        -H "Authorization: token $GITHUB_TOKEN" \
        "https://api.github.com/repos/${GITHUB_ORG}/${repo_name}")
    
    if [ "$http_code" = "404" ]; then
        # 创建 GitHub 仓库
        log "Creating GitHub repo: $repo_name"
        curl -s -X POST -H "Authorization: token $GITHUB_TOKEN" \
            -H "Content-Type: application/json" \
            "https://api.github.com/orgs/${GITHUB_ORG}/repos" \
            -d "{\"name\":\"$repo_name\",\"private\":true}" > /dev/null
    fi
    
    # 配置远程并推送
    cd "$repo"
    git remote remove github 2>/dev/null
    git remote add --mirror=push github \
        "https://${GITHUB_TOKEN}@github.com/${GITHUB_ORG}/${repo_name}.git"
    
    if git push github 2>> "$LOG_FILE"; then
        log "SUCCESS: $repo_name mirrored"
    else
        log "FAILED: $repo_name mirror failed"
    fi
done

log "Mirror sync completed"

9.2.4 Cron 定时同步

# 编辑 crontab
crontab -e

# 每 6 小时同步一次
0 */6 * * * /opt/scripts/batch-mirror-push.sh >> /var/log/git-mirror-cron.log 2>&1

9.3 从 GitHub 拉取镜像

9.3.1 拉取开源项目

# 创建裸仓库
git init --bare /opt/git/mirror/linux.git

# 设置为镜像
cd /opt/git/mirror/linux.git
git remote add origin https://github.com/torvalds/linux.git

# 首次拉取(完整镜像)
git fetch origin

# 后续同步
git fetch origin --prune

9.3.2 自动同步拉取镜像

#!/bin/bash
# sync-pull-mirrors.sh

MIRROR_DIR="/opt/git/mirror"
LOG_FILE="/var/log/git-pull-mirror.log"

find "$MIRROR_DIR" -name "*.git" -type d | while read repo; do
    echo "$(date '+%Y-%m-%d %H:%M:%S') Syncing: $(basename $repo .git)" >> "$LOG_FILE"
    
    cd "$repo"
    git fetch origin --prune --force 2>> "$LOG_FILE"
done

echo "$(date '+%Y-%m-%d %H:%M:%S') Pull mirror sync completed" >> "$LOG_FILE"

9.4 Gitea/Forgejo 镜像管理

9.4.1 仓库镜像配置

# 列出仓库的镜像配置
curl -s -H "Authorization: token $TOKEN" \
  "$GITEA_URL/api/v1/repos/owner/repo" | jq '.mirror_interval, .mirror_updated'

# 手动触发镜像同步
curl -s -X POST -H "Authorization: token $TOKEN" \
  "$GITEA_URL/api/v1/repos/owner/repo/push-mirrors/sync"

9.4.2 批量镜像管理

#!/bin/bash
# manage-mirrors.sh

GITEA_URL="https://git.example.com"
TOKEN="your_token"

# 列出所有启用了镜像的仓库
repos=$(curl -s -H "Authorization: token $TOKEN" \
  "$GITEA_URL/api/v1/repos/search?limit=100&mirror=true" | jq -r '.data[] | .full_name')

echo "=== 镜像仓库列表 ==="
for repo in $repos; do
    mirror_info=$(curl -s -H "Authorization: token $TOKEN" \
        "$GITEA_URL/api/v1/repos/$repo" | jq '{
        name: .full_name,
        has_push_mirror: .has_pull_requests,
        mirror_interval: .mirror_interval
    }')
    echo "$mirror_info"
done

9.5 备份策略

9.5.1 备份层级

层级内容频率保留时间
L1: 仓库数据Git 仓库文件每日30 天
L2: 数据库用户/权限/Issue/PR每日30 天
L3: 配置文件服务器配置变更时永久
L4: 附件/LFS上传的文件和 LFS每日30 天
L5: 完整快照系统快照/镜像每周90 天

9.5.2 裸仓库备份

#!/bin/bash
# backup-repos.sh

set -euo pipefail

GIT_ROOT="/opt/git"
BACKUP_DIR="/var/backups/git"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="git_repos_${DATE}"
BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}"
RETENTION_DAYS=30

mkdir -p "$BACKUP_PATH"

echo "=== Git 仓库备份 ==="
echo "时间: $(date)"
echo "源目录: $GIT_ROOT"
echo "备份目录: $BACKUP_PATH"

# 备份仓库数据
echo "正在备份仓库数据..."
for repo in $(find "$GIT_ROOT" -name "*.git" -type d); do
    repo_name=$(echo "$repo" | sed "s|$GIT_ROOT/||" | tr '/' '_')
    
    # 使用 git bundle 创建完整备份
    bundle_file="${BACKUP_PATH}/${repo_name}.bundle"
    cd "$repo"
    
    # 创建包含所有分支和标签的 bundle
    if git bundle create "$bundle_file" --all 2>/dev/null; then
        echo "  ✅ $repo_name ($(du -sh "$bundle_file" | cut -f1))"
    else
        # 空仓库可能没有引用,使用 tar 备份
        tar czf "${BACKUP_PATH}/${repo_name}.tar.gz" -C "$(dirname $repo)" "$(basename $repo)"
        echo "  ✅ $repo_name (tar, 空仓库)"
    fi
done

# 创建压缩包
echo "正在创建压缩包..."
cd "$BACKUP_DIR"
tar czf "${BACKUP_NAME}.tar.gz" "$BACKUP_NAME"
rm -rf "$BACKUP_PATH"

BACKUP_SIZE=$(du -sh "${BACKUP_DIR}/${BACKUP_NAME}.tar.gz" | cut -f1)
echo "备份完成: ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz ($BACKUP_SIZE)"

# 清理旧备份
echo "清理 ${RETENTION_DAYS} 天前的备份..."
find "$BACKUP_DIR" -name "git_repos_*.tar.gz" -mtime +$RETENTION_DAYS -delete

# 显示当前备份列表
echo ""
echo "=== 当前备份 ==="
ls -lh "$BACKUP_DIR"/git_repos_*.tar.gz 2>/dev/null || echo "无备份文件"

9.5.3 Gitea 备份

#!/bin/bash
# backup-gitea.sh

set -euo pipefail

GITEA_USER="git"
GITEA_ROOT="/var/lib/gitea"
BACKUP_DIR="/var/backups/gitea"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30

mkdir -p "$BACKUP_DIR"

echo "=== Gitea 备份 ==="

# 方式一:使用 Gitea 内置 dump 命令
echo "使用 Gitea dump 创建备份..."
sudo -u "$GITEA_USER" gitea dump \
    --config /etc/gitea/app.ini \
    --file "${BACKUP_DIR}/gitea-dump-${DATE}.zip" \
    --verbose

echo "Gitea dump 完成: ${BACKUP_DIR}/gitea-dump-${DATE}.zip"
echo "大小: $(du -sh ${BACKUP_DIR}/gitea-dump-${DATE}.zip | cut -f1)"

# 方式二:手动备份各组件(更灵活)
echo ""
echo "手动备份各组件..."

# 备份仓库
echo "  备份仓库数据..."
tar czf "${BACKUP_DIR}/gitea-repos-${DATE}.tar.gz" \
    -C "$(dirname $GITEA_ROOT)/data" gitea-repositories 2>/dev/null || true

# 备份数据库
echo "  备份数据库..."
DB_TYPE=$(sudo -u "$GITEA_USER" grep "DB_TYPE" /etc/gitea/app.ini | awk -F= '{print $2}' | tr -d ' ')

case "$DB_TYPE" in
    postgres|pgsql)
        sudo -u "$GITEA_USER" pg_dump giteadb | gzip > "${BACKUP_DIR}/gitea-db-${DATE}.sql.gz"
        ;;
    mysql)
        mysqldump -u gitea -p giteadb | gzip > "${BACKUP_DIR}/gitea-db-${DATE}.sql.gz"
        ;;
    sqlite3)
        cp "${GITEA_ROOT}/data/gitea.db" "${BACKUP_DIR}/gitea-db-${DATE}.db"
        ;;
esac

# 备份配置
echo "  备份配置文件..."
tar czf "${BACKUP_DIR}/gitea-config-${DATE}.tar.gz" \
    /etc/gitea/app.ini /etc/gitea/app.ini.bak 2>/dev/null || true

# 备份 LFS 和附件
echo "  备份 LFS 和附件..."
tar czf "${BACKUP_DIR}/gitea-lfs-${DATE}.tar.gz" \
    -C "${GITEA_ROOT}/data" lfs 2>/dev/null || true
tar czf "${BACKUP_DIR}/gitea-attachments-${DATE}.tar.gz" \
    -C "${GITEA_ROOT}/data" attachments 2>/dev/null || true

echo ""
echo "=== 备份完成 ==="
ls -lh "${BACKUP_DIR}"/*${DATE}* 2>/dev/null

# 清理旧备份
echo ""
echo "清理 ${RETENTION_DAYS} 天前的备份..."
find "$BACKUP_DIR" -mtime +$RETENTION_DAYS -delete

echo "备份脚本执行完成"

9.5.4 GitLab 备份

#!/bin/bash
# backup-gitlab.sh

set -euo pipefail

BACKUP_DIR="/var/backups/gitlab"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30

mkdir -p "$BACKUP_DIR"

echo "=== GitLab 备份 ==="

# 创建 GitLab 完整备份
echo "创建 GitLab 备份..."
sudo gitlab-backup create STRATEGY=copy

# 备份配置文件(备份命令不包含配置)
echo "备份配置文件..."
sudo cp /etc/gitlab/gitlab.rb "${BACKUP_DIR}/gitlab.rb.${DATE}"
sudo cp /etc/gitlab/gitlab-secrets.json "${BACKUP_DIR}/gitlab-secrets.json.${DATE}"
sudo tar czf "${BACKUP_DIR}/gitlab-ssl-${DATE}.tar.gz" \
    /etc/gitlab/ssl 2>/dev/null || true

# 备份 Nginx 配置
sudo tar czf "${BACKUP_DIR}/gitlab-nginx-${DATE}.tar.gz" \
    /var/opt/gitlab/nginx 2>/dev/null || true

echo ""
echo "=== 备份完成 ==="
echo "GitLab 备份: /var/opt/gitlab/backups/"
ls -lh /var/opt/gitlab/backups/*_${DATE%_*}* 2>/dev/null || echo "(新备份可能在稍后完成)"
echo ""
echo "配置备份: $BACKUP_DIR"
ls -lh "${BACKUP_DIR}"/*${DATE}*

# 清理旧备份
echo ""
echo "清理 ${RETENTION_DAYS} 天前的备份..."
find "$BACKUP_DIR" -mtime +$RETENTION_DAYS -delete
sudo find /var/opt/gitlab/backups -mtime +$RETENTION_DAYS -delete

9.6 备份验证和恢复

9.6.1 定期验证备份

#!/bin/bash
# verify-backup.sh - 验证备份完整性

BACKUP_FILE="${1:?用法: $0 <backup-file>}"
VERIFY_DIR="/tmp/git-backup-verify"

rm -rf "$VERIFY_DIR"
mkdir -p "$VERIFY_DIR"

echo "=== 验证备份: $BACKUP_FILE ==="

# 解压备份
tar xzf "$BACKUP_FILE" -C "$VERIFY_DIR"

# 检查 bundle 文件
for bundle in "$VERIFY_DIR"/*.bundle; do
    [ -f "$bundle" ] || continue
    
    repo_name=$(basename "$bundle" .bundle)
    echo -n "验证 $repo_name ... "
    
    if git bundle verify "$bundle" 2>/dev/null; then
        echo "✅ 通过"
    else
        echo "❌ 失败"
    fi
done

# 清理
rm -rf "$VERIFY_DIR"
echo ""
echo "验证完成"

9.6.2 从 Bundle 恢复仓库

#!/bin/bash
# restore-from-bundle.sh

BUNDLE_FILE="${1:?用法: $0 <bundle-file> <target-dir>}"
TARGET_DIR="${2:?用法: $0 <bundle-file> <target-dir>}"

echo "从 $BUNDLE_FILE 恢复到 $TARGET_DIR"

# 创建裸仓库
git init --bare "$TARGET_DIR"

# 从 bundle 克隆
cd "$TARGET_DIR"
git bundle unbundle "$BUNDLE_FILE"

# 或使用 clone
git clone "$BUNDLE_FILE" "$TARGET_DIR"

echo "恢复完成: $TARGET_DIR"

9.6.3 Gitea 恢复

# 从 Gitea dump 恢复
# 1. 停止 Gitea
sudo systemctl stop gitea

# 2. 解压 dump
unzip gitea-dump-20260510.zip -d /tmp/gitea-restore

# 3. 恢复仓库
sudo cp -r /tmp/gitea-restore/repos/* /var/lib/gitea/data/gitea-repositories/

# 4. 恢复数据库
# PostgreSQL:
sudo -u postgres psql giteadb < /tmp/gitea-restore/gitea-db.sql

# SQLite:
cp /tmp/gitea-restore/gitea.db /var/lib/gitea/data/gitea.db

# 5. 恢复配置
sudo cp /tmp/gitea-restore/app.ini /etc/gitea/app.ini

# 6. 恢复附件和 LFS
sudo cp -r /tmp/gitea-restore/lfs/* /var/lib/gitea/data/lfs/
sudo cp -r /tmp/gitea-restore/attachments/* /var/lib/gitea/data/attachments/

# 7. 修复权限
sudo chown -R git:git /var/lib/gitea/

# 8. 启动 Gitea
sudo systemctl start gitea

9.7 异地备份

9.7.1 Rsync 远程备份

#!/bin/bash
# remote-backup.sh - 使用 Rsync 同步到远程服务器

BACKUP_DIR="/var/backups/git"
REMOTE_HOST="backup.example.com"
REMOTE_DIR="/backups/git-server"
SSH_KEY="/root/.ssh/backup_key"

rsync -avz --delete \
    -e "ssh -i $SSH_KEY -p 22" \
    "$BACKUP_DIR/" \
    "${REMOTE_HOST}:${REMOTE_DIR}/"

echo "远程同步完成: $(date)"

9.7.2 对象存储备份

#!/bin/bash
# s3-backup.sh - 备份到 S3 兼容存储

BACKUP_FILE="$1"
S3_BUCKET="s3://my-git-backups"
S3_ENDPOINT="https://s3.example.com"

# 使用 rclone 或 aws-cli
rclone copy "$BACKUP_FILE" "$S3_BUCKET/git-server/" \
    --s3-endpoint="$S3_ENDPOINT" \
    --progress

echo "S3 上传完成"

9.8 扩展阅读


本章小结

学到了什么关键要点
Push Mirror仓库推送到 GitHub/GitLab,支持平台内置和 Git 远程方式
Pull Mirror从外部仓库拉取镜像,用于引入开源项目
备份策略仓库数据、数据库、配置文件、附件/LFS 分层备份
备份验证定期验证备份完整性,测试恢复流程
异地备份Rsync 到远程服务器、对象存储(S3)

下一章:第 10 章 - CI/CD 集成 — 构建完整的持续集成和持续部署流水线。