Git 服务器搭建完全指南 / 第 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 界面配置:
- 进入仓库 → Settings → Mirror Settings
- 选择 Push Mirror
- 填写目标 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:ghp_xxxxxxxxxxxx@github.com" >> ~/.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 集成 — 构建完整的持续集成和持续部署流水线。