一键批量扫描并更新指定目录下所有 Docker Compose 项目,自动拉取镜像、重建容器、清理无用镜像,实时显示容器状态并输出完整更新统计报告。
1panel 定时更新 compose 项目完美。bash <(curl -sL gitee.com/meimolihan/linux-command_sh/raw/master/docker_batch_update.sh) /vol1/1000/compose
./脚本名.sh /自定义目录./脚本名.sh "当前目录"#!/bin/bash
gl_hui='\e[37m'
gl_hong='\033[31m'
gl_lv='\033[32m'
gl_huang='\033[33m'
gl_lan='\033[34m'
gl_bai='\033[97m'
gl_zi='\033[35m'
gl_bufan='\033[96m'
gl_info='\033[94m'
gl_reset='\033[0m'
TARGET_DIR="${1:-.}"
COMPOSE_CMD=$(command -v docker-compose || echo "docker compose")
COUNT=0
SUCCESS=0
FAIL=0
UPDATED_PROJECTS=()
NO_UPDATE_PROJECTS=()
display_container_status() {
local container_count=0
local running_count=0
$COMPOSE_CMD ps --format "table {{.Name}}\t{{.State}}\t{{.Ports}}" 2>/dev/null | while IFS= read -r line; do
if [[ "$line" =~ "NAME" ]] || [[ "$line" =~ "----" ]] || [[ "$line" =~ "PORTS" ]] || \
[[ "$line" =~ "COMMAND" ]] || [[ -z "$line" ]] || [[ "$line" =~ "^-+" ]]; then
continue
fi
local container_name=$(echo "$line" | awk '{print $1}')
local container_state=$(echo "$line" | awk '{print $2}')
if [[ -n "$container_name" ]] && [[ -n "$container_state" ]] && \
[[ "$container_name" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]+$ ]] && \
[[ "$container_state" =~ ^(Up|Exited|Restarting|Paused|Dead|Created|Stopped)$ ]]; then
((container_count++))
case "$container_state" in
"Up")
((running_count++))
;;
esac
fi
done
if [[ $container_count -eq 0 ]]; then
local containers=$($COMPOSE_CMD ps -q 2>/dev/null)
if [[ -n "$containers" ]]; then
container_count=$(echo "$containers" | wc -l)
running_count=$($COMPOSE_CMD ps --filter status=running -q 2>/dev/null | wc -l)
fi
fi
if [[ $container_count -gt 0 ]]; then
echo -e "${gl_bai}容器状态: ${gl_lv}✓${gl_bai} 发现 ${gl_bufan}$container_count ${gl_bai}个容器,其中 ${gl_bufan}$running_count ${gl_bai}个在运行"
else
echo -e "${gl_bai}容器状态: ${gl_huang}⚠️ 未发现运行中的容器${gl_bai}"
fi
}
check_for_updates() {
local dir="$1"
local project_name="$2"
local pull_exit_code="$3"
local up_exit_code="$4"
local pull_output="$5"
local up_output="$6"
local has_update=false
local update_type=""
if [[ $pull_exit_code -eq 0 ]]; then
if echo "$pull_output" | grep -q -E "Downloaded newer image|Status: Downloaded newer image"; then
has_update=true
update_type="镜像更新"
fi
fi
if [[ $up_exit_code -eq 0 ]]; then
if echo "$up_output" | grep -q -E "Recreating|Creating|Starting|Started"; then
has_update=true
if [[ -n "$update_type" ]]; then
update_type="镜像+容器更新"
else
update_type="容器更新"
fi
fi
fi
if [[ "$has_update" == "true" ]]; then
UPDATED_PROJECTS+=("$project_name")
echo -e "${gl_lv}✅ 更新成功 ${gl_huang}(${update_type})${gl_bai}"
else
NO_UPDATE_PROJECTS+=("$project_name")
echo -e "${gl_lv}✅ 更新完成 (无变化)${gl_bai}"
fi
}
echo -e ""
start_time=$(date '+%F %T'); start_ts=$(date +%s)
echo -e "${gl_bai}开始更新时间:${gl_lv}$start_time${gl_bai}"
echo -e "${gl_bai}开始更新 ${gl_huang}$TARGET_DIR${gl_bai} 下的所有 Docker Compose 项目${gl_hong}.${gl_huang}.${gl_lv}.${gl_reset}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_reset}"
while IFS= read -r dir; do
[[ -z "$dir" ]] && continue
((COUNT++))
echo -e ""
echo -e "${gl_bai}[${gl_bufan}$COUNT${gl_bai}]${gl_zi} >>> 处理目录: ${gl_huang}$dir${gl_bai}"
# echo -e "${gl_bai}[${gl_bufan}$COUNT${gl_bai}]${gl_zi} >>> 处理目录: ${gl_huang}$(basename "$dir")${gl_bai}"
# echo -e "${gl_bai}项目路径: ${gl_huang}$dir${gl_bai}"
if cd "$dir" 2>/dev/null; then
DIR_NAME=$(basename "$dir")
PROJECT_NAME="$DIR_NAME"
if grep -q "^name:" docker-compose.yml docker-compose.yaml compose.yml 2>/dev/null; then
COMPOSE_FILE=$(ls docker-compose.yml docker-compose.yaml compose.yml 2>/dev/null | head -1)
if [[ -n "$COMPOSE_FILE" ]]; then
COMPOSE_NAME=$(grep "^name:" "$COMPOSE_FILE" | head -1 | sed 's/^name:[[:space:]]*//' | tr -d '\r'"'"'"')
[[ -n "$COMPOSE_NAME" ]] && PROJECT_NAME="$COMPOSE_NAME"
fi
fi
if [[ -f ".env" ]] && grep -q "COMPOSE_PROJECT_NAME" .env; then
ENV_NAME=$(grep "COMPOSE_PROJECT_NAME" .env | head -1 | cut -d'=' -f2- | tr -d '\r'"'"'"')
[[ -n "$ENV_NAME" ]] && PROJECT_NAME="$ENV_NAME"
fi
echo -e "${gl_bai}项目名称: ${gl_huang}$PROJECT_NAME${gl_bai}"
echo -e "${gl_bai}正在拉取镜像中${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
PULL_OUTPUT=$($COMPOSE_CMD pull --quiet 2>&1)
PULL_EXIT_CODE=$?
echo -e "${gl_bai}正在更新容器中${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
UP_OUTPUT=$($COMPOSE_CMD up -d --remove-orphans 2>&1)
UP_EXIT_CODE=$?
check_for_updates "$dir" "$PROJECT_NAME" "$PULL_EXIT_CODE" "$UP_EXIT_CODE" "$PULL_OUTPUT" "$UP_OUTPUT"
if [[ $PULL_EXIT_CODE -eq 0 ]] && [[ $UP_EXIT_CODE -eq 0 ]]; then
display_container_status
docker image prune -f >/dev/null 2>&1 && echo -e "${gl_bai}镜像清理: ${gl_lv}♻️ 镜像清理完成${gl_bai}"
((SUCCESS++))
else
echo -e "${gl_hong}❌ 更新失败${gl_bai}"
[[ $PULL_EXIT_CODE -ne 0 ]] && echo -e "${gl_huang}Pull错误: ${gl_hui}$(echo "$PULL_OUTPUT" | head -5)${gl_bai}"
[[ $UP_EXIT_CODE -ne 0 ]] && echo -e "${gl_huang}Up错误: ${gl_hui}$(echo "$UP_OUTPUT" | head -5)${gl_bai}"
((FAIL++))
fi
else
echo -e "${gl_huang}⚠️ 无法进入目录${gl_bai}"
((FAIL++))
fi
done < <(find "$TARGET_DIR" -maxdepth 3 \( -name "docker-compose.yml" -o -name "docker-compose.yaml" -o -name "compose.yml" \) 2>/dev/null | xargs dirname 2>/dev/null | sort -u)
# 显示统计结果(保持不变)
echo ""
echo -e "${gl_lv}✅ 批量更新完成!"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_reset}"
echo -e " ${gl_bai}统计信息${gl_bai}"
echo -e " ${gl_bai}总计项目: ${gl_huang}$COUNT${gl_bai}"
echo -e " ${gl_bai}总计成功: ${gl_lv}$SUCCESS${gl_bai}"
echo -e " ${gl_bai}总计失败: ${gl_hong}$FAIL${gl_bai}"
if [[ ${#UPDATED_PROJECTS[@]} -gt 0 ]]; then
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_reset}"
echo -e " ${gl_bai}有实际更新的项目 (${gl_lv}${#UPDATED_PROJECTS[@]}${gl_bai}个):"
for i in "${!UPDATED_PROJECTS[@]}"; do
project_name="${UPDATED_PROJECTS[$i]}"
[[ -n "$project_name" ]] && [[ "$project_name" != "unknown_project" ]] && \
echo -e " ${gl_lv}✓${gl_bai} $((i+1)). $project_name"
done
else
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_reset}"
echo -e " ${gl_hui}无项目更新${gl_bai}"
fi
if [[ ${#NO_UPDATE_PROJECTS[@]} -gt 0 ]]; then
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_reset}"
echo -e " ${gl_bai}无更新的项目 (${gl_bufan}${#NO_UPDATE_PROJECTS[@]}${gl_bai}个):"
for i in "${!NO_UPDATE_PROJECTS[@]}"; do
project_name="${NO_UPDATE_PROJECTS[$i]}"
[[ -n "$project_name" ]] && [[ "$project_name" != "unknown_project" ]] && \
echo -e " ${gl_lv}○${gl_bai} $((i+1)). $project_name"
done
fi
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_reset}"
end_time=$(date '+%F %T'); end_ts=$(date +%s)
total=$((end_ts - start_ts))
printf -v dur "%d时%02d分%02d秒" $((total/3600)) $(((total%3600)/60)) $((total%60))
echo -e "${gl_bai}结束更新时间:${gl_hong}$end_time${gl_bai}"
echo -e "${gl_bai}更新用时共计:${gl_lv}$dur${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_reset}"
if [[ ${#UPDATED_PROJECTS[@]} -gt 0 ]]; then
echo -e "${gl_zi}💡 提示: 有 ${gl_lv}${#UPDATED_PROJECTS[@]}${gl_zi} 个项目已更新,建议进行健康检查${gl_bai}"
fi
if [[ $FAIL -gt 0 ]]; then
echo -e "${gl_huang}⚠️ 注意: 有 ${gl_hong}$FAIL${gl_huang} 个项目更新失败,请检查日志${gl_bai}"
fi
new_script="new_test.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"