Files
docker-backup/bin/backup.sh
Wang Defa 4d00283654 feat: 实现 Docker 备份系统,支持远程一键安装
实现功能:
- 文件夹和 MySQL 容器数据库备份
- tar.gz 压缩和自动清理旧备份
- systemd 定时任务集成
- 远程一键安装脚本(通过 Gitea 仓库)
- 完整的 llmdoc 文档系统

安装方式:
bash <(curl -sL https://gitea.bcde.io/wangdefa/docker-backup/raw/branch/main/install.sh)

配置文件位置:/etc/docker-backup/config.yml
命令:docker-backup, docker-backup-cleanup
2025-12-25 15:02:07 +08:00

401 lines
11 KiB
Bash
Executable File

#!/bin/bash
###############################################################################
# Docker Backup Script
# 功能:备份指定文件夹和 MySQL 容器数据库
###############################################################################
set -e # 遇到错误立即退出
set -o pipefail # 管道命令任何一个失败都返回失败
# 脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 配置文件路径(系统级配置)
CONFIG_FILE="${CONFIG_FILE:-/etc/docker-backup/config.yml}"
# 临时目录
TEMP_DIR=""
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
###############################################################################
# 日志函数
###############################################################################
log() {
local level=$1
shift
local message="$@"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "[${timestamp}] [${level}] ${message}"
# 如果配置了日志文件,同时写入文件
if [[ -n "${LOG_FILE}" ]] && [[ "${LOGGING_ENABLED}" == "true" ]]; then
echo "[${timestamp}] [${level}] ${message}" >> "${LOG_FILE}"
fi
}
log_info() {
log "INFO" "${GREEN}$@${NC}"
}
log_warn() {
log "WARN" "${YELLOW}$@${NC}"
}
log_error() {
log "ERROR" "${RED}$@${NC}"
}
###############################################################################
# 清理函数
###############################################################################
cleanup() {
if [[ -n "${TEMP_DIR}" ]] && [[ -d "${TEMP_DIR}" ]]; then
log_info "清理临时目录: ${TEMP_DIR}"
rm -rf "${TEMP_DIR}"
fi
}
# 注册清理函数
trap cleanup EXIT INT TERM
###############################################################################
# 检查依赖
###############################################################################
check_dependencies() {
log_info "检查依赖工具..."
local missing_deps=()
# 检查 yq
if ! command -v yq &> /dev/null; then
missing_deps+=("yq")
fi
# 检查 tar
if ! command -v tar &> /dev/null; then
missing_deps+=("tar")
fi
# 检查 gzip
if ! command -v gzip &> /dev/null; then
missing_deps+=("gzip")
fi
# 检查 docker
if ! command -v docker &> /dev/null; then
missing_deps+=("docker")
fi
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_error "缺少以下依赖工具: ${missing_deps[*]}"
log_error "请运行 install.sh 安装依赖"
exit 1
fi
log_info "依赖检查完成"
}
###############################################################################
# 加载配置
###############################################################################
load_config() {
log_info "加载配置文件: ${CONFIG_FILE}"
if [[ ! -f "${CONFIG_FILE}" ]]; then
log_error "配置文件不存在: ${CONFIG_FILE}"
exit 1
fi
# 读取备份基础配置
OUTPUT_DIR=$(yq eval '.backup.output_dir' "${CONFIG_FILE}")
BACKUP_PREFIX=$(yq eval '.backup.prefix' "${CONFIG_FILE}")
# 读取清理策略
RETENTION_ENABLED=$(yq eval '.backup.retention.enabled' "${CONFIG_FILE}")
KEEP_DAYS=$(yq eval '.backup.retention.keep_days' "${CONFIG_FILE}")
# 读取文件夹备份配置
FOLDERS_ENABLED=$(yq eval '.folders.enabled' "${CONFIG_FILE}")
# 读取 MySQL 配置
MYSQL_ENABLED=$(yq eval '.mysql.enabled' "${CONFIG_FILE}")
MYSQL_CONTAINER=$(yq eval '.mysql.container_name' "${CONFIG_FILE}")
MYSQL_USERNAME=$(yq eval '.mysql.username' "${CONFIG_FILE}")
MYSQL_PASSWORD=$(yq eval '.mysql.password' "${CONFIG_FILE}")
# 读取日志配置
LOGGING_ENABLED=$(yq eval '.logging.enabled' "${CONFIG_FILE}")
LOG_FILE=$(yq eval '.logging.log_file' "${CONFIG_FILE}")
# 创建输出目录
mkdir -p "${OUTPUT_DIR}"
# 创建日志目录
if [[ "${LOGGING_ENABLED}" == "true" ]] && [[ -n "${LOG_FILE}" ]]; then
mkdir -p "$(dirname "${LOG_FILE}")"
fi
log_info "配置加载完成"
}
###############################################################################
# 备份文件夹
###############################################################################
backup_folders() {
if [[ "${FOLDERS_ENABLED}" != "true" ]]; then
log_info "文件夹备份未启用,跳过"
return 0
fi
log_info "开始备份文件夹..."
# 创建临时目录
local temp_backup_dir="${TEMP_DIR}/folders"
mkdir -p "${temp_backup_dir}"
# 获取要备份的文件夹数量
local source_count=$(yq eval '.folders.sources | length' "${CONFIG_FILE}")
if [[ "${source_count}" == "0" ]] || [[ "${source_count}" == "null" ]]; then
log_warn "未配置要备份的文件夹"
return 0
fi
# 构建排除参数
local exclude_args=""
local exclude_count=$(yq eval '.folders.excludes | length' "${CONFIG_FILE}")
if [[ "${exclude_count}" != "0" ]] && [[ "${exclude_count}" != "null" ]]; then
for i in $(seq 0 $((exclude_count - 1))); do
local exclude_pattern=$(yq eval ".folders.excludes[$i]" "${CONFIG_FILE}")
exclude_args+=" --exclude='${exclude_pattern}'"
done
fi
# 备份每个文件夹
local folders_tar="${temp_backup_dir}/folders.tar.gz"
local tar_sources=""
for i in $(seq 0 $((source_count - 1))); do
local source_dir=$(yq eval ".folders.sources[$i]" "${CONFIG_FILE}")
if [[ ! -d "${source_dir}" ]]; then
log_warn "源目录不存在,跳过: ${source_dir}"
continue
fi
tar_sources+=" ${source_dir}"
log_info "添加备份源: ${source_dir}"
done
if [[ -z "${tar_sources}" ]]; then
log_warn "没有有效的备份源目录"
return 0
fi
# 执行打包
log_info "开始打包文件夹..."
eval "tar -czf '${folders_tar}' ${exclude_args} ${tar_sources}" 2>&1 | while read line; do
log_info "$line"
done
if [[ -f "${folders_tar}" ]]; then
log_info "文件夹备份完成: ${folders_tar}"
echo "${folders_tar}"
else
log_error "文件夹备份失败"
return 1
fi
}
###############################################################################
# 备份 MySQL 数据库
###############################################################################
backup_mysql() {
if [[ "${MYSQL_ENABLED}" != "true" ]]; then
log_info "MySQL 备份未启用,跳过"
return 0
fi
log_info "开始备份 MySQL 数据库..."
# 检查容器是否存在且运行中
if ! docker ps --format '{{.Names}}' | grep -q "^${MYSQL_CONTAINER}$"; then
log_error "MySQL 容器不存在或未运行: ${MYSQL_CONTAINER}"
return 1
fi
# 创建临时目录
local temp_mysql_dir="${TEMP_DIR}/mysql"
mkdir -p "${temp_mysql_dir}"
# 获取要备份的数据库列表
local databases=$(yq eval '.mysql.databases' "${CONFIG_FILE}")
local mysql_dump="${temp_mysql_dir}/mysql.sql.gz"
if [[ "${databases}" == "all" ]] || [[ "${databases}" == "null" ]]; then
# 备份所有数据库
log_info "备份所有数据库..."
docker exec "${MYSQL_CONTAINER}" mysqldump \
--single-transaction \
--quick \
--skip-lock-tables \
-u"${MYSQL_USERNAME}" \
-p"${MYSQL_PASSWORD}" \
--all-databases \
| gzip > "${mysql_dump}"
else
# 备份指定数据库
local db_count=$(yq eval '.mysql.databases | length' "${CONFIG_FILE}")
local db_list=""
for i in $(seq 0 $((db_count - 1))); do
local db_name=$(yq eval ".mysql.databases[$i]" "${CONFIG_FILE}")
db_list+=" ${db_name}"
log_info "添加数据库: ${db_name}"
done
log_info "备份数据库: ${db_list}"
docker exec "${MYSQL_CONTAINER}" mysqldump \
--single-transaction \
--quick \
--skip-lock-tables \
-u"${MYSQL_USERNAME}" \
-p"${MYSQL_PASSWORD}" \
--databases ${db_list} \
| gzip > "${mysql_dump}"
fi
if [[ -f "${mysql_dump}" ]]; then
log_info "MySQL 备份完成: ${mysql_dump}"
echo "${mysql_dump}"
else
log_error "MySQL 备份失败"
return 1
fi
}
###############################################################################
# 合并备份文件
###############################################################################
merge_backups() {
local folders_tar=$1
local mysql_dump=$2
log_info "开始合并备份文件..."
# 生成时间戳
local timestamp=$(date '+%Y%m%d-%H%M%S')
local final_backup="${OUTPUT_DIR}/${BACKUP_PREFIX}-${timestamp}.tar.gz"
# 要打包的文件列表
local files_to_pack=""
if [[ -n "${folders_tar}" ]] && [[ -f "${folders_tar}" ]]; then
files_to_pack+=" ${folders_tar}"
fi
if [[ -n "${mysql_dump}" ]] && [[ -f "${mysql_dump}" ]]; then
files_to_pack+=" ${mysql_dump}"
fi
if [[ -z "${files_to_pack}" ]]; then
log_error "没有需要打包的文件"
return 1
fi
# 合并打包
tar -czf "${final_backup}" -C "${TEMP_DIR}" \
$(basename "${folders_tar}" 2>/dev/null || true) \
$(basename "${mysql_dump}" 2>/dev/null || true)
if [[ -f "${final_backup}" ]]; then
local file_size=$(du -h "${final_backup}" | cut -f1)
log_info "备份完成: ${final_backup} (大小: ${file_size})"
echo "${final_backup}"
else
log_error "合并备份失败"
return 1
fi
}
###############################################################################
# 清理旧备份
###############################################################################
cleanup_old_backups() {
if [[ "${RETENTION_ENABLED}" != "true" ]]; then
log_info "自动清理未启用,跳过"
return 0
fi
log_info "开始清理旧备份..."
if [[ "${KEEP_DAYS}" != "null" ]] && [[ "${KEEP_DAYS}" =~ ^[0-9]+$ ]]; then
log_info "删除 ${KEEP_DAYS} 天前的备份文件..."
# 查找并删除旧文件
find "${OUTPUT_DIR}" -name "${BACKUP_PREFIX}-*.tar.gz" -type f -mtime +${KEEP_DAYS} -print | while read old_file; do
log_info "删除旧备份: ${old_file}"
rm -f "${old_file}"
done
fi
log_info "清理完成"
}
###############################################################################
# 主函数
###############################################################################
main() {
log_info "=========================================="
log_info "Docker Backup 开始执行"
log_info "=========================================="
# 检查依赖
check_dependencies
# 加载配置
load_config
# 创建临时目录
TEMP_DIR=$(mktemp -d -t docker-backup.XXXXXX)
log_info "临时目录: ${TEMP_DIR}"
# 备份文件夹
local folders_tar=$(backup_folders)
# 备份 MySQL
local mysql_dump=$(backup_mysql)
# 合并备份
local final_backup=$(merge_backups "${folders_tar}" "${mysql_dump}")
# 清理旧备份
cleanup_old_backups
log_info "=========================================="
log_info "Docker Backup 执行完成"
log_info "备份文件: ${final_backup}"
log_info "=========================================="
}
# 执行主函数
main "$@"