diff --git a/README.md b/README.md index f736c43..e47d628 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ | `deploy-monitor.sh` | 独立部署 monitor-ai 告警监控 | 云端 | | `edit-site-cc.sh` | www.tlyq.cc 官网内容管理 | 云端 | | `check-compliance.sh` | D1-D8 合规检查(LESSONS-LEARNED 自动化) | 本地 | +| `docker-cache-cleanup.sh` | Docker 缓存清理(构建缓存、悬空镜像、测试镜像) | 本地/云端 | ### 备份与恢复(`backup/` 目录) @@ -101,6 +102,40 @@ bash scripts/backup/restore.sh --local --full ... # 本地灾难恢复(新服 > 详细文档见 [backup/README.md](backup/README.md) +## docker-cache-cleanup.sh — Docker 缓存清理 + +自动检测运行环境:通过 `curl ip.sb` 获取当前公网 IP,与生产环境服务器 IP 列表匹配。 +- 在生产服务器上运行:直接执行 Docker 命令 +- 在本地运行:通过 SSH 连接服务器执行 + +```bash +bash scripts/docker-cache-cleanup.sh # 默认:保留 3 小时内的缓存 +bash scripts/docker-cache-cleanup.sh --aggressive # 激进:保留 1 小时内 +bash scripts/docker-cache-cleanup.sh --conservative # 保守:保留 6 小时内 +bash scripts/docker-cache-cleanup.sh --full # 完全:删除所有缓存 +``` + +清理内容: +- **dangling 镜像**:无标签或未被引用的中间层 +- **测试镜像**:以 `test-` 开头的镜像 +- **构建缓存**:根据模式保留不同时长 +- **磁盘紧张处理**:使用率 > 85% 时自动深度清理 + +配置:在脚本开头的 `PRODUCTION_SERVER_IPS` 数组中添加/删除服务器 IP。 + +```bash +PRODUCTION_SERVER_IPS=( + "43.133.38.210" # txjp (腾讯云北京) + # "159.75.83.209" # tgz (腾讯云广州) +) +``` + +定时清理(可选): +```bash +# 每天凌晨 3 点执行保守清理 +ssh txjp "crontab -l 2>/dev/null; echo '0 3 * * * /root/docker/scripts/docker-cache-cleanup.sh --conservative' | crontab -" +``` + ## 备份存储路径 | 位置 | 路径 | diff --git a/deploy-ai-readme.md b/deploy-ai-readme.md index 8054adf..980ad79 100644 --- a/deploy-ai-readme.md +++ b/deploy-ai-readme.md @@ -43,7 +43,7 @@ bash scripts/deploy-ai.sh [选项] 5. **依赖安装**:服务器 `npm install --prefer-offline` 6. **环境变量**:写入生产环境所需的环境变量到 `.env` 7. **构建**:服务器 `npm run build` -8. **清理**:删除 Alpine musl 残留(sharp、swc) +8. **清理**:删除 Alpine musl 残留(sharp、swc),清理 Docker 构建缓存(调用 `docker-cache-cleanup.sh`) 9. **重启容器**:`docker compose up -d` + `docker compose restart` 10. **快照保存**:保存本次源码 MD5 用于下次比对 diff --git a/deploy-ai.sh b/deploy-ai.sh index aca519d..8de4042 100755 --- a/deploy-ai.sh +++ b/deploy-ai.sh @@ -16,6 +16,11 @@ # 不使用 set -e,改用显式错误处理,避免管道命令导致意外退出 +# ============================================================ +# 脚本目录 +# ============================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + # ============================================================ # 颜色和日志 # ============================================================ @@ -684,28 +689,9 @@ EOF echo 'shared 符号链接已恢复' fi" - # 7. 清理构建产生的悬空镜像和缓存(根据磁盘使用率动态调整) + # 7. 清理构建产生的悬空镜像和缓存(调用独立清理脚本) log "清理构建缓存..." - # 清理悬空镜像(无标签或未被引用的中间层) - ssh txjp "docker image prune -f 2>/dev/null" || warn "docker image prune 失败" - - # 根据磁盘使用率动态调整构建缓存清理策略 - DISK_USAGE=$(ssh txjp "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "50") - if [ "$DISK_USAGE" -gt 95 ]; then - warn "磁盘使用率 ${DISK_USAGE}%,执行紧急清理" - ssh txjp "docker builder prune -f 2>/dev/null || true" - # 截断容器日志(占磁盘大户) - ssh txjp "truncate -s 0 /var/lib/docker/containers/\$(docker inspect --format '{{.Id}}' ${CONTAINER} 2>/dev/null)/\*-json.log 2>/dev/null || true" - elif [ "$DISK_USAGE" -gt 85 ]; then - info "磁盘使用率 ${DISK_USAGE}%,执行激进清理(6小时)" - ssh txjp "docker builder prune -f --filter 'until=6h' 2>/dev/null || true" - elif [ "$DISK_USAGE" -gt 70 ]; then - info "磁盘使用率 ${DISK_USAGE}%,执行正常清理(24小时)" - ssh txjp "docker builder prune -f --filter 'until=24h' 2>/dev/null || true" - else - info "磁盘使用率 ${DISK_USAGE}%,执行保守清理(48小时)" - ssh txjp "docker builder prune -f --filter 'until=48h' 2>/dev/null || true" - fi + bash "${SCRIPT_DIR}/docker-cache-cleanup.sh" --default # 8. 保存快照 ssh txjp "mkdir -p ${SNAPSHOT_DIR} && echo '$local_md5' > ${SNAPSHOT_FILE}" diff --git a/docker-cache-cleanup.sh b/docker-cache-cleanup.sh new file mode 100755 index 0000000..f7277b1 --- /dev/null +++ b/docker-cache-cleanup.sh @@ -0,0 +1,200 @@ +#!/bin/bash +# Docker 缓存清理脚本 +# 用法: +# ./docker-cache-cleanup.sh # 默认清理(保留 3 小时内的缓存) +# ./docker-cache-cleanup.sh --aggressive # 激进清理(保留 1 小时内) +# ./docker-cache-cleanup.sh --conservative # 保守清理(保留 6 小时内) +# ./docker-cache-cleanup.sh --full # 完全清理(删除所有缓存) + +set -euo pipefail + +# ============================================================ +# 配置 +# ============================================================ +REMOTE_HOST="${REMOTE_HOST:-txjp}" +CLEANUP_MODE="${1:---default}" +RETENTION_HOURS=3 # 默认保留 3 小时 + +# 生产环境服务器 IP 列表(用于判断是否在服务器本地运行) +# 新增服务器时,只需在此添加 IP 地址即可 +PRODUCTION_SERVER_IPS=( + "43.133.38.210" # txjp (腾讯云北京) + # "159.75.83.209" # tgz (腾讯云广州) - 如需添加 + # "10.0.0.1" # 其他服务器 - 如需添加 +) + +# 检测运行环境: +# 通过 curl ip.sb 获取当前公网 IP,与生产环境服务器 IP 列表匹配 +IS_LOCAL=false + +# 获取当前公网 IP +CURRENT_IP=$(curl -s --connect-timeout 5 ip.sb 2>/dev/null || echo "") + +if [ -n "$CURRENT_IP" ]; then + # 遍历生产环境服务器 IP 列表,检查是否匹配 + for SERVER_IP in "${PRODUCTION_SERVER_IPS[@]}"; do + if [ "$CURRENT_IP" = "$SERVER_IP" ]; then + IS_LOCAL=true + break + fi + done +fi + +# 如果是本地运行,不需要 SSH +if [ "$IS_LOCAL" = true ]; then + REMOTE_HOST="" +fi + +# 颜色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# ============================================================ +# 日志函数 +# ============================================================ +log() { echo -e "${GREEN}[$(date '+%H:%M:%S')]${NC} $*"; } +warn() { echo -e "${YELLOW}[$(date '+%H:%M:%S')] WARNING:${NC} $*"; } +error() { echo -e "${RED}[$(date '+%H:%M:%S')] ERROR:${NC} $*" >&2; } + +# ============================================================ +# 清理模式设置 +# ============================================================ +case "$CLEANUP_MODE" in + --aggressive) + RETENTION_HOURS=1 + log "激进清理模式:保留 ${RETENTION_HOURS} 小时内的缓存" + ;; + --conservative) + RETENTION_HOURS=6 + log "保守清理模式:保留 ${RETENTION_HOURS} 小时内的缓存" + ;; + --full) + log "完全清理模式:删除所有缓存" + RETENTION_HOURS=0 + ;; + *) + log "默认清理模式:保留 ${RETENTION_HOURS} 小时内的缓存" + ;; +esac + +# ============================================================ +# 辅助函数 +# ============================================================ +# 执行命令(根据是否本地决定是否使用 SSH) +run_cmd() { + if [ "$IS_LOCAL" = true ]; then + eval "$@" + else + ssh "$REMOTE_HOST" "$@" + fi +} + +# 获取磁盘使用率 +get_disk_usage() { + run_cmd "df / --output=pcent | tail -1 | tr -d ' %'" 2>/dev/null || echo "0" +} + +# 获取构建缓存大小 +get_cache_size() { + run_cmd "docker system df --format '{{.Size}}' 2>/dev/null | head -1" || echo "0" +} + +# 获取镜像数量 +get_image_count() { + run_cmd "docker images -q 2>/dev/null | wc -l" || echo "0" +} + +# ============================================================ +# 清理函数 +# ============================================================ +# 清理 dangling 镜像(无标签或未被引用的中间层) +cleanup_dangling_images() { + log "清理 dangling 镜像..." + run_cmd "docker image prune -f 2>/dev/null" || warn "docker image prune 失败" +} + +# 清理测试镜像(以 test- 开头的镜像) +cleanup_test_images() { + log "清理测试镜像..." + local test_images=$(run_cmd "docker images --format '{{.Repository}}:{{.Tag}}' | grep -E '^test-' 2>/dev/null" || true) + if [ -n "$test_images" ]; then + echo "$test_images" | while read -r img; do + log " 删除: $img" + done + run_cmd "docker images --format '{{.Repository}}:{{.Tag}}' | grep -E '^test-' | xargs -r docker rmi -f 2>/dev/null" || true + else + log " 无测试镜像" + fi +} + +# 清理构建缓存 +cleanup_build_cache() { + if [ "$RETENTION_HOURS" -eq 0 ]; then + log "清理所有构建缓存..." + run_cmd "docker builder prune -af 2>/dev/null" || warn "docker builder prune 失败" + else + log "清理 ${RETENTION_HOURS} 小时前的构建缓存..." + run_cmd "docker builder prune -f --filter 'until=${RETENTION_HOURS}h' 2>/dev/null" || warn "docker builder prune 失败" + fi +} + +# 根据磁盘使用率决定是否需要深度清理 +cleanup_by_disk_usage() { + local DISK_USAGE=$(get_disk_usage) + log "当前磁盘使用率: ${DISK_USAGE}%" + + if [ "$DISK_USAGE" -gt 85 ]; then + warn "磁盘使用率 ${DISK_USAGE}% > 85%,执行深度清理" + # 清理所有构建缓存 + run_cmd "docker builder prune -af 2>/dev/null" || true + # 清理容器日志 + log "截断容器日志..." + run_cmd "truncate -s 0 /var/lib/docker/containers/\$(docker ps -q)/\*-json.log 2>/dev/null" || true + elif [ "$DISK_USAGE" -gt 75 ]; then + cleanup_build_cache + else + log "磁盘使用率正常,仅执行常规清理" + cleanup_build_cache + fi +} + +# ============================================================ +# 打印清理结果 +# ============================================================ +print_result() { + local DISK_AFTER=$(get_disk_usage) + local CACHE_AFTER=$(get_cache_size) + local IMAGE_AFTER=$(get_image_count) + echo "" + log "========== 清理完成 ==========" + log "磁盘使用率: ${DISK_AFTER}%" + log "构建缓存大小: ${CACHE_AFTER}" + log "镜像数量: ${IMAGE_AFTER}" +} + +# ============================================================ +# 主函数 +# ============================================================ +main() { + local DISK_BEFORE=$(get_disk_usage) + local CACHE_BEFORE=$(get_cache_size) + local IMAGE_BEFORE=$(get_image_count) + + echo "" + log "========== Docker 缓存清理 ==========" + log "清理前 - 磁盘使用率: ${DISK_BEFORE}%, 缓存大小: ${CACHE_BEFORE}, 镜像数量: ${IMAGE_BEFORE}" + echo "" + + # 执行清理 + cleanup_dangling_images + cleanup_test_images + cleanup_by_disk_usage + + # 打印结果 + print_result +} + +# 执行主函数 +main "$@" diff --git a/sites-manage-local.sh b/sites-manage-local.sh index 7f73533..b032278 100755 --- a/sites-manage-local.sh +++ b/sites-manage-local.sh @@ -38,10 +38,9 @@ MONITOR_PORT=6181 WWW_CC_PORT=5173 CLOUD_CC_PORT=5174 TOKEN_CC_PORT=5175 -GITEA_PORT=3000 +GITEA_PORT=6182 is_port() { lsof -i :"$1" >/dev/null 2>&1; } -is_running() { docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^$1$"; } # 检查 Docker 服务是否运行 check_docker() { @@ -56,16 +55,21 @@ check_docker() { # ============================================================ docker_start() { local name=$1 dir=$2 - if is_running $name; then + if [ "$(cd "$dir" && docker compose ps -q --status running 2>/dev/null)" ]; then warn "$name 已在运行" else log "启动 $name..." - cd "$dir" && docker compose up -d 2>/dev/null && log "$name 已启动" || err "$name 启动失败" + cd "$dir" && docker compose up -d 2>/dev/null || { err "$name 启动失败"; return 1; } + for i in $(seq 1 30); do + [ "$(cd "$dir" && docker compose ps -q --status running 2>/dev/null)" ] && { log "$name 已启动"; return 0; } + sleep 1 + done + err "$name 启动异常(可能处于重启循环,请检查 docker compose logs)" fi } docker_stop() { local name=$1 dir=$2 - if is_running $name; then + if [ "$(cd "$dir" && docker compose ps -q --status running --status restarting 2>/dev/null)" ]; then log "停止 $name..." cd "$dir" && docker compose down 2>/dev/null && log "$name 已停止" || err "$name 停止失败" else @@ -102,28 +106,28 @@ node_stop() { start_all() { for name in "$@"; do case $name in - www-cc) docker_start www-local "$BASE_DIR/www-cc" ;; - cloud-cc) docker_start cloud-cc "$BASE_DIR/cloud-cc" ;; - token-cc) docker_start token-cc "$BASE_DIR/token-cc" ;; - www-ai) docker_start www-local-ai "$BASE_DIR/www-ai" ;; + www-cc) docker_start www-cc "$BASE_DIR/cc-site/www-cc" ;; + cloud-cc) docker_start cloud-cc "$BASE_DIR/cc-site/cloud-cc" ;; + token-cc) docker_start token-cc "$BASE_DIR/cc-site/token-cc" ;; + www-ai) docker_start www-ai "$BASE_DIR/www-ai" ;; cloud-ai) docker_start cloud-ai "$BASE_DIR/cloud-ai" ;; token-ai) docker_start token-ai "$BASE_DIR/token-ai" ;; - issue) node_start issue-ai "$ISSUE_DIR" $ISSUE_PORT "/tmp/issue-ai-dev.log" + issue) node_start issue "$ISSUE_DIR" $ISSUE_PORT "/tmp/issue-ai-dev.log" if ! is_port $ASSETS_PORT; then - node_start assets-ai "$ASSETS_DIR" $ASSETS_PORT "/tmp/assets-ai-dev.log" + node_start assets "$ASSETS_DIR" $ASSETS_PORT "/tmp/assets-ai-dev.log" warn "已自动启动 assets-ai(issue 依赖 assets API)" fi ;; - assets) node_start assets-ai "$ASSETS_DIR" $ASSETS_PORT "/tmp/assets-ai-dev.log" ;; - ldap) docker_start lldap "$LDAP_DIR" + assets) node_start assets "$ASSETS_DIR" $ASSETS_PORT "/tmp/assets-ai-dev.log" ;; + ldap) docker_start ldap "$LDAP_DIR" if is_port $LDAP_WEB_PORT; then log "LLDAP Web UI: http://127.0.0.1:$LDAP_WEB_PORT" fi ;; - oa) node_start oa-ai "$OA_DIR" $OA_PORT "/tmp/oa-ai-dev.log" ;; - sso) docker_start authelia "$SSO_DIR" ;; - gitea) docker_start gitea-ai "$GITEA_DIR" ;; - monitor) node_start monitor-ai "$MONITOR_DIR" $MONITOR_PORT "/tmp/monitor-ai-dev.log" ;; + oa) node_start oa "$OA_DIR" $OA_PORT "/tmp/oa-ai-dev.log" ;; + sso) docker_start sso "$SSO_DIR" ;; + gitea) docker_start gitea "$GITEA_DIR" ;; + monitor) node_start monitor "$MONITOR_DIR" $MONITOR_PORT "/tmp/monitor-ai-dev.log" ;; cc) start_all www-cc cloud-cc token-cc ;; - ai) start_all ldap www-ai cloud-ai token-ai issue assets oa sso gitea monitor ;; + ai) start_all www-ai cloud-ai token-ai issue assets ldap oa sso monitor gitea ;; all) start_all cc ai ;; *) err "未知站点: $name" ;; esac @@ -133,21 +137,21 @@ start_all() { stop_all() { for name in "$@"; do case $name in - www-cc) docker_stop www-local "$BASE_DIR/www-cc" ;; - cloud-cc) docker_stop cloud-cc "$BASE_DIR/cloud-cc" ;; - token-cc) docker_stop token-cc "$BASE_DIR/token-cc" ;; - www-ai) docker_stop www-local-ai "$BASE_DIR/www-ai" ;; + www-cc) docker_stop www-cc "$BASE_DIR/cc-site/www-cc" ;; + cloud-cc) docker_stop cloud-cc "$BASE_DIR/cc-site/cloud-cc" ;; + token-cc) docker_stop token-cc "$BASE_DIR/cc-site/token-cc" ;; + www-ai) docker_stop www-ai "$BASE_DIR/www-ai" ;; cloud-ai) docker_stop cloud-ai "$BASE_DIR/cloud-ai" ;; token-ai) docker_stop token-ai "$BASE_DIR/token-ai" ;; - issue) node_stop issue-ai $ISSUE_PORT ;; - assets) node_stop assets-ai $ASSETS_PORT ;; - ldap) docker_stop lldap "$LDAP_DIR" ;; - oa) node_stop oa-ai $OA_PORT ;; - sso) docker_stop authelia "$SSO_DIR" ;; - gitea) docker_stop gitea-ai "$GITEA_DIR" ;; - monitor) node_stop monitor-ai $MONITOR_PORT ;; + issue) node_stop issue $ISSUE_PORT ;; + assets) node_stop assets $ASSETS_PORT ;; + ldap) docker_stop ldap "$LDAP_DIR" ;; + oa) node_stop oa $OA_PORT ;; + sso) docker_stop sso "$SSO_DIR" ;; + gitea) docker_stop gitea "$GITEA_DIR" ;; + monitor) node_stop monitor $MONITOR_PORT ;; cc) stop_all www-cc cloud-cc token-cc ;; - ai) stop_all ldap www-ai cloud-ai token-ai issue assets oa sso gitea monitor ;; + ai) stop_all www-ai cloud-ai token-ai issue assets ldap oa sso monitor gitea ;; all) stop_all cc ai ;; *) err "未知站点: $name" ;; esac @@ -170,9 +174,9 @@ show_status() { local expanded=() for t in "${targets[@]}"; do case $t in - all) eval "expanded=(www-cc cloud-cc token-cc ldap www-ai cloud-ai token-ai issue assets oa sso gitea monitor)" ;; + all) eval "expanded=(www-cc cloud-cc token-cc www-ai cloud-ai token-ai issue assets ldap oa sso monitor gitea)" ;; cc) eval "expanded=(\${expanded[@]} www-cc cloud-cc token-cc)" ;; - ai) eval "expanded=(\${expanded[@]} ldap www-ai cloud-ai token-ai issue assets oa sso gitea monitor)" ;; + ai) eval "expanded=(\${expanded[@]} www-ai cloud-ai token-ai issue assets ldap oa sso monitor gitea)" ;; *) expanded+=($t) ;; esac done @@ -203,17 +207,31 @@ show_status() { } local running=0 - for name in "${expanded[@]}"; do - local port=$(get_port $name) - if [ -n "$port" ] && is_port $port; then - printf " ${GREEN}●${NC} %-12s 运行中 (port %d)\n" "$name" $port - running=$((running + 1)) - else - printf " ${RED}●${NC} %-12s 未运行\n" "$name" - fi - done - echo "" + # 按 cc/ai 分组输出 + _status_group() { + local label=$1; shift + local group=() gname n + for gname in "$@"; do + for n in "${expanded[@]}"; do + [ "$n" = "$gname" ] && group+=("$n") && break + done + done + [ ${#group[@]} -eq 0 ] && return + printf " ${CYAN}%s站点:${NC}\n" "$label" + for gname in "${group[@]}"; do + local port=$(get_port $gname) + if [ -n "$port" ] && is_port $port; then + printf " ${GREEN}●${NC} %-12s 运行中 (port %d)\n" "$gname" $port + running=$((running + 1)) + else + printf " ${RED}●${NC} %-12s 未运行\n" "$gname" + fi + done + echo "" + } + _status_group "cc " www-cc cloud-cc token-cc + _status_group "ai " www-ai cloud-ai token-ai issue assets ldap oa sso monitor gitea printf "${CYAN}=========================================${NC}\n" printf "${CYAN} 访问地址${NC}\n" printf "${CYAN}=========================================${NC}\n" @@ -227,13 +245,13 @@ show_status() { echo " www-ai http://127.0.0.1:6173" echo " cloud-ai http://127.0.0.1:6174" echo " token-ai http://127.0.0.1:6175" + echo " issue http://127.0.0.1:$ISSUE_PORT" + echo " assets http://127.0.0.1:$ASSETS_PORT" echo " ldap http://127.0.0.1:$LDAP_WEB_PORT (LLDAP Web UI)" echo " oa http://127.0.0.1:$OA_PORT" echo " sso http://127.0.0.1:$SSO_PORT (Authelia OIDC)" - echo " issue http://127.0.0.1:$ISSUE_PORT" - echo " assets http://127.0.0.1:$ASSETS_PORT" - echo " gitea http://127.0.0.1:$GITEA_PORT" echo " monitor http://127.0.0.1:$MONITOR_PORT" + echo " gitea http://127.0.0.1:$GITEA_PORT" echo "" }