技术博客

Docker 镜像生命周期管理:自动清理策略、Harbor GC 与镜像版本治理

系统讲解 Docker 镜像生命周期的全链路管理:本地镜像的自动清理(定时 cron + docker system prune)、Harbor 私有仓库的 GC 原理与配置(标记删除 vs 实际回收两阶段)、镜像保留策略的设计(语义版本永久保留 + 构建版本保留最近 N 个)、BuildKit 构建缓存的清理与保留、镜像晋升与废弃的治理流程,以及各环境镜像的生命周期差异(dev/staging/prod 不同保留策略)。

Docker镜像管理HarborGC生命周期自动清理DevOps

没有清理策略的镜像仓库是“垃圾场”:Harbor 磁盘爆满、CI runner 磁盘空间耗尽、工程师不知道哪个 tag 在生产运行——这些问题都源于缺乏系统性的镜像生命周期管理。本文建立一套从本地到仓库的完整清理与治理方案。

镜像生命周期全貌

构建                    存储                    部署                    清理
────────────────────────────────────────────────────────────────────────
CI 构建               Harbor 仓库             生产运行中              过期清理
  ↓ push                ├── v1.0.0 (永久)        ↓                    GC 回收
本地构建缓存            ├── v1.1.0 (永久)     环境 tag 标记         BuildKit prune
  ↓                    ├── abc1234 (30天)     production/staging    本地 prune
CI runner 缓存         ├── def5678 (30天)                          
                       └── latest (7天)

一、本地镜像清理

1.1 手动清理命令

# 查看当前磁盘占用(操作前必看)
docker system df
# TYPE          TOTAL   ACTIVE  SIZE    RECLAIMABLE
# Images        67      8       23.4GB  18.2GB (78%)
# Containers    12      4       1.2GB   0.8GB (67%)
# Local Volumes 8       3       45GB    20GB (44%)
# Build Cache   -       -       4.3GB   4.3GB

# 分步清理(生产操作,不要一把 prune -a)
# 步骤1:清理停止的容器(安全,无数据丢失风险)
docker container prune -f

# 步骤2:清理悬空镜像(<none>:<none>,安全)
docker image prune -f

# 步骤3:清理未使用的 volume(危险!会删数据,先确认)
docker volume ls --filter dangling=true    # 先列出
docker volume prune -f                     # 再清理

# 步骤4:清理 BuildKit 缓存(控制保留量)
docker buildx prune --keep-storage=10gb -f

# 步骤5:清理所有未使用的镜像(包括有 tag 但没有容器引用的)
# 危险!会删除所有未运行容器引用的镜像,包括预拉取的基础镜像
docker image prune -a -f

# 全量清理(危险!谨慎执行)
docker system prune -a --volumes -f

1.2 自动清理脚本

#!/bin/bash
# /usr/local/bin/docker-cleanup.sh
# 定时执行,安全地清理 Docker 资源

set -e
LOG_FILE=/var/log/docker-cleanup.log
exec >> "$LOG_FILE" 2>&1

echo "=== $(date -u +%Y-%m-%dT%H:%M:%SZ) 开始清理 ==="

# 清理前磁盘使用
BEFORE=$(docker system df --format '{{.Size}}' | paste -sd+ | bc 2>/dev/null || echo "N/A")
echo "清理前总占用:$(docker system df | grep -E 'Images|Containers|Volumes|Build Cache' | awk '{sum+=$4} END{print sum/1024/1024 "GB"}')"

# 清理停止的容器(超过 24 小时的)
echo "[容器] 清理已停止 24 小时以上的容器..."
docker container prune -f --filter "until=24h"

# 清理悬空镜像
echo "[镜像] 清理悬空镜像..."
DANGLING=$(docker images -q --filter dangling=true | wc -l)
if [ "$DANGLING" -gt 0 ]; then
    docker image prune -f
    echo "  已清理 $DANGLING 个悬空镜像"
fi

# 清理超过 7 天的未使用镜像(dev 环境可以更激进)
# 生产机器不执行这步,避免误删预热镜像
if [ "${ENVIRONMENT}" = "ci" ] || [ "${ENVIRONMENT}" = "dev" ]; then
    echo "[镜像] CI/Dev 环境:清理 7 天以上未使用的镜像..."
    docker image prune -a -f --filter "until=168h"    # 168h = 7天
fi

# 清理 BuildKit 缓存(保留最近 10GB)
echo "[构建缓存] 清理 BuildKit 缓存(保留 10GB)..."
docker buildx prune -f --keep-storage=10gb 2>/dev/null || true

# 清理未使用的网络
echo "[网络] 清理未使用的 Docker 网络..."
docker network prune -f

# 查看清理后效果
echo "清理后磁盘使用:"
docker system df

echo "=== 清理完成 ==="
# 设置 cron(每天凌晨 3 点执行)
cat > /etc/cron.d/docker-cleanup << 'EOF'
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
ENVIRONMENT=production

0 3 * * * root /usr/local/bin/docker-cleanup.sh
EOF

chmod 644 /etc/cron.d/docker-cleanup
chmod +x /usr/local/bin/docker-cleanup.sh

二、Harbor GC(垃圾回收)

2.1 Harbor 删除的两阶段

Harbor 的镜像删除是两阶段操作:

阶段1:标记删除(立即生效)
  删除 tag → Harbor 数据库标记该 manifest 为待删除
  Harbor UI 中不再显示该 tag
  但底层 blob(实际镜像层数据)仍在磁盘上!
  
  为什么不立即删除 blob?
  → 多个镜像可能共享同一个 layer(同一个 blob)
  → 必须确认没有任何引用后才能安全删除

阶段2:GC(实际磁盘回收)
  Harbor 扫描所有 manifest → 找出没有任何引用的 blob
  删除孤立 blob → 磁盘空间真正被释放
  
  GC 期间 Harbor 进入只读模式(不能 push)
  → 建议在业务低峰期(如凌晨)执行

2.2 GC 配置(Harbor UI)

管理入口:
  Harbor Web UI → 系统管理 → 垃圾清理

配置项:
  ┌─────────────────────────────────────────────────┐
  │ 调度(Schedule)                                 │
  │   ○ 无调度(手动触发)                           │
  │   ● 每天(Daily)       时间: 02:00              │
  │   ○ 每周(Weekly)                               │
  │   ○ 自定义 Cron                                  │
  │                                                   │
  │ 删除未打 tag 的 manifest: ✓                      │
  │ 并发数: 2                                         │
  └─────────────────────────────────────────────────┘

2.3 Harbor API 触发 GC

# 手动触发 GC(维护窗口时执行)
HARBOR_URL=https://harbor.company.com
HARBOR_USER=admin
HARBOR_PASS=Harbor12345

# 触发 GC
curl -X POST \
    -u "${HARBOR_USER}:${HARBOR_PASS}" \
    -H "Content-Type: application/json" \
    "${HARBOR_URL}/api/v2.0/system/gc/schedule" \
    -d '{
        "schedule": {
            "type": "Manual"
        },
        "delete_untagged": true
    }'

# 查看 GC 状态
curl -s \
    -u "${HARBOR_USER}:${HARBOR_PASS}" \
    "${HARBOR_URL}/api/v2.0/system/gc" \
    | jq '.[] | {id: .id, status: .job_status, freed: .job_parameters}'

# 查看 GC 日志
curl -s \
    -u "${HARBOR_USER}:${HARBOR_PASS}" \
    "${HARBOR_URL}/api/v2.0/system/gc/1/log" | head -50

三、镜像保留策略设计

3.1 Harbor 保留策略(Retention Policy)

# 通过 Harbor API 创建保留策略

# 推荐策略组合(backend 项目):
# 规则1:永久保留语义版本 tag(v*)
# 规则2:保留最近 20 个构建 tag(非 v* 前缀)
# 规则3:dev 项目:保留最近 10 个(快速过期)

HARBOR_URL=https://harbor.company.com
PROJECT_ID=2  # backend 项目的 ID(从 Harbor API 获取)

curl -X POST \
    -u admin:Harbor12345 \
    -H "Content-Type: application/json" \
    "${HARBOR_URL}/api/v2.0/retentions" \
    -d '{
        "algorithm": "or",
        "rules": [
            {
                "priority": 1,
                "disabled": false,
                "action": "retain",
                "template": "always",
                "tag_selectors": [{
                    "kind": "doublestar",
                    "decoration": "matches",
                    "pattern": "v**"
                }],
                "scope_selectors": {
                    "repository": [{
                        "kind": "doublestar",
                        "decoration": "repoMatches",
                        "pattern": "**"
                    }]
                }
            },
            {
                "priority": 2,
                "disabled": false,
                "action": "retain",
                "template": "latestActiveK",
                "params": {"latestN": 20},
                "tag_selectors": [{
                    "kind": "doublestar",
                    "decoration": "excludes",
                    "pattern": "v**"
                }],
                "scope_selectors": {
                    "repository": [{
                        "kind": "doublestar",
                        "decoration": "repoMatches",
                        "pattern": "**"
                    }]
                }
            }
        ],
        "trigger": {
            "kind": "Schedule",
            "settings": {"cron": "0 1 * * 0"}
        },
        "scope": {
            "level": "project",
            "ref": 2
        }
    }'

3.2 各环境镜像保留策略对比

环境           保留周期    策略
──────────────────────────────────────────────────────
生产(prod)
  语义版本      永久        v* 匹配,永不删除
  构建 tag      60 天       commit SHA,部署后 60 天
  environment   保留最近 10 production/staging tag

测试(staging)
  构建 tag      30 天       commit SHA,30 天
  latest        永远 1 个   每次覆盖

开发(dev)
  构建 tag      7 天        快速过期
  feature/      3 天        特性分支镜像
  latest        永远 1 个

构建缓存
  buildcache    不限        永远保留(加速构建)
  (只有 blob 有 GC)

3.3 不可变 tag(防止覆盖)

# 为语义版本 tag 设置不可变规则(Immutable Tag)
# Harbor UI:项目 → 配置 → 不可变标签

# API 方式
curl -X POST \
    -u admin:Harbor12345 \
    -H "Content-Type: application/json" \
    "${HARBOR_URL}/api/v2.0/projects/backend/immutabletags" \
    -d '{
        "selector": {
            "kind": "doublestar",
            "decoration": "repoMatches",
            "pattern": "**"
        },
        "tag_selectors": [{
            "kind": "doublestar",
            "decoration": "matches",
            "pattern": "v**"
        }]
    }'

# 验证:尝试推送同名 tag 会失败
docker tag myapp:v1.0.0-new harbor.company.com/backend/api:v1.0.0
docker push harbor.company.com/backend/api:v1.0.0
# Error response from daemon: tag v1.0.0 is immutable

四、BuildKit 构建缓存管理

# 查看 BuildKit 缓存使用
docker buildx du
# ID                          RECLAIMABLE   SIZE        LAST ACCESSED
# md5xxxxxx                   true          1.2GB       2 hours ago
# sha256:yyy...               true          850MB       1 day ago
# Total:                                    15.3GB

# 保留最近 N GB,清理其余
docker buildx prune --keep-storage=10gb -f

# 只清理超过 X 天的缓存
docker buildx prune --filter "until=168h" -f  # 7 天

# 清理所有缓存(重置,构建速度暂时下降)
docker buildx prune -a -f

# CI 中的缓存策略(GitLab CI 示例)
# .gitlab-ci.yml
build:
  script:
    - |
      docker buildx build \
        --cache-from type=registry,ref=harbor.company.com/cache/api:buildcache \
        --cache-to type=registry,ref=harbor.company.com/cache/api:buildcache,mode=max \
        -t harbor.company.com/backend/api:${CI_COMMIT_SHORT_SHA} \
        --push .
    
    # CI runner 本地缓存(runner 磁盘不超 20GB)
    - docker buildx prune --keep-storage=20gb -f

五、镜像废弃与下架流程

#!/bin/bash
# deprecate-image.sh — 将镜像标记为废弃并通知团队
# 用于废弃旧版本,但不立即删除(给依赖方时间迁移)

IMAGE=$1      # harbor.company.com/backend/api
VERSION=$2    # v1.0.0
REASON=$3     # "已被 v1.1.0 替代"

HARBOR_URL=https://harbor.company.com
PROJECT=backend
REPO=api

echo "废弃镜像:${IMAGE}:${VERSION}"
echo "原因:${REASON}"

# 1. 获取镜像 digest
DIGEST=$(docker manifest inspect ${IMAGE}:${VERSION} \
    --verbose 2>/dev/null | jq -r '.Descriptor.digest')

echo "对应 Digest:${DIGEST}"

# 2. 在 Harbor 中添加废弃标签(仅供参考,不阻断拉取)
# 使用自定义 label 标注废弃状态
docker buildx imagetools create \
    --tag ${IMAGE}:${VERSION}-deprecated \
    ${IMAGE}:${VERSION}

# 3. 记录废弃日志
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) DEPRECATED ${IMAGE}:${VERSION} → ${REASON}" \
    >> /var/log/image-lifecycle.log

# 4. 通知(发钉钉 webhook)
WEBHOOK_URL="https://oapi.dingtalk.com/robot/send?access_token=xxx"
curl -s -X POST "${WEBHOOK_URL}" \
    -H "Content-Type: application/json" \
    -d "{
        \"msgtype\": \"text\",
        \"text\": {
            \"content\": \"[镜像废弃通知] ${IMAGE}:${VERSION} 已废弃。原因:${REASON}。请尽快迁移到新版本。\"
        }
    }"

echo "✅ 废弃流程完成"
echo "   30天后执行实际删除:harbor-delete-image.sh ${IMAGE} ${VERSION}"

六、磁盘使用监控

# Prometheus 监控 Harbor 磁盘使用率
# Harbor 暴露 /metrics 端点(需要在管理界面开启)

# Prometheus scrape 配置
# scrape_configs:
#   - job_name: harbor
#     static_configs:
#       - targets: ['harbor.company.com:80']
#     metrics_path: /metrics
#     basic_auth:
#       username: admin
#       password: Harbor12345

# 关键指标
# harbor_storage_used_bytes → Harbor 已用存储
# harbor_db_latency_ms → 数据库延迟

# 告警规则
# - alert: HarborDiskUsageHigh
#   expr: harbor_storage_used_bytes / harbor_storage_total_bytes > 0.80
#   annotations:
#     summary: "Harbor 磁盘使用率超过 80%,请执行 GC 或扩容"

# 也可以用脚本简单监控
check_harbor_disk() {
    USAGE=$(df -h /data/harbor | awk 'NR==2 {print $5}' | tr -d '%')
    if [ "$USAGE" -gt 80 ]; then
        echo "警告:Harbor 磁盘使用率 ${USAGE}%,请立即清理"
        # 触发 GC
        curl -X POST -u admin:Harbor12345 \
            https://harbor.company.com/api/v2.0/system/gc/schedule \
            -H "Content-Type: application/json" \
            -d '{"schedule":{"type":"Manual"},"delete_untagged":true}'
    fi
}

小结

镜像生命周期管理的核心是:让该保留的永远保留,让该清理的自动清理

实际操作中,三类镜像处理方式截然不同:语义版本镜像(v1.2.3)永久保留,这是你能回溯历史版本和紧急回滚的保障;CI 构建镜像(commit SHA)保留最近 N 个即可;开发分支镜像 7 天自动过期。

Harbor GC 是“真正释放空间”的关键步骤——仅仅删除 tag 不会释放磁盘,必须运行 GC 才能回收 blob 层占用的空间。建议每周自动执行一次,在业务低峰时段运行(GC 期间 Harbor 只读)。