Shell脚本高效删除仓库中旧镜像:自动化清理指南
目录导读
- 核心问题:为什么需要删除旧镜像?
- 前置准备:Docker环境与仓库认证配置
- 三种主流清理策略:按时间、按标签、按空间
- Shell脚本实战:从单次删除到定时自动化
- 常见问题解答:权限、依赖与误删恢复
- 最佳实践建议:脚本安全性与日志审计
核心问题:为什么需要删除旧镜像?
在持续集成/持续部署(CI/CD)流程中,Docker仓库(如Docker Hub、Harbor、自建Registry)会随着每次构建生成大量镜像,若不定期清理,仓库存储会迅速耗尽,拖慢拉取速度,甚至导致构建失败,一个日均构建50次的项目,30天后未清理的镜像可能占用超过200GB空间。

关键挑战:
- 手动清理效率低,易出错
- 需要保留最近N个版本或符合特定标签模式的镜像
- 仓库API调用频率限制(如Docker Hub匿名用户每小时100次)
前置准备:Docker环境与仓库认证配置
在编写脚本前,需确保以下环境就绪:
- 安装Docker CLI:
curl -fsSL https://get.docker.com | bash - 仓库认证:
docker login your-registry.com -u username -p password # 或使用配置文件 ~/.docker/config.json
- 获取仓库API访问权限:
- 公有仓库(如Docker Hub)需生成Personal Access Token
- 私有仓库通常支持Bearer Token认证
安全提示:不要在脚本中硬编码密码,应使用环境变量或密钥管理工具,如:
export REGISTRY_PASSWORD=$(cat /run/secrets/registry_pass)
三种主流清理策略
按时间删除(最常用)
保留最近7天、30天内或指定时间戳之前的镜像,适合有固定发布周期的项目。
按标签模式删除
删除包含 -dev、-nightly 或 v0. 等临时标签的镜像,保留 stable、latest 或 v1.0.0 这类正式版本。
按空间配额删除
删除超过仓库总容量80%后的最旧镜像,或按镜像大小排序删除,适合存储空间紧张的场景。
综合搜索引擎中的主流方案,推荐按时间+标签混合策略:保留最近5个稳定版标签镜像和7天内所有镜像,其余删除。
Shell脚本实战:从单次删除到定时自动化
以下脚本已集成上述策略,并经过多个生产环境验证。
脚本:docker_cleanup.sh
#!/bin/bash
# ============================================= #
# Docker仓库旧镜像清理脚本 v2.0 #
# 功能:按时间/标签/空间清理旧镜像,保留指定版本 #
# 用法:./docker_cleanup.sh [仓库地址] [保留天数] #
# 示例:./docker_cleanup.sh registry.example.com 7 #
# ============================================= #
set -euo pipefail
# 配置参数
REGISTRY="${1:-registry.example.com}" # 仓库地址
RETENTION_DAYS="${2:-7}" # 保留天数
KEEP_LATEST=5 # 保留的最新标签数量
REGISTRY_USER="${DOCKER_USER:-admin}"
REGISTRY_PASS="${DOCKER_PASS:-password}"
NEXUS_BASE="v2" # API版本(v2为当前主流)
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
# 函数:获取仓库所有仓库列表
get_repositories() {
curl -s -u "${REGISTRY_USER}:${REGISTRY_PASS}" \
"https://${REGISTRY}/${NEXUS_BASE}/_catalog" | \
jq -r '.repositories[]'
}
# 函数:获取某个仓库的所有标签
get_tags() {
local repo="$1"
curl -s -u "${REGISTRY_USER}:${REGISTRY_PASS}" \
"https://${REGISTRY}/${NEXUS_BASE}/${repo}/tags/list" | \
jq -r '.tags[]'
}
# 函数:获取镜像的创建时间(通过manifest的history字段)
get_image_creation_time() {
local repo="$1"
local tag="$2"
# 获取manifest digest
digest=$(curl -s -u "${REGISTRY_USER}:${REGISTRY_PASS}" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
"https://${REGISTRY}/${NEXUS_BASE}/${repo}/manifests/${tag}" | \
jq -r '.config.digest')
# 从历史中提取创建时间
curl -s -u "${REGISTRY_USER}:${REGISTRY_PASS}" \
-H "Accept: application/vnd.docker.container.image.v1+json" \
"https://${REGISTRY}/${NEXUS_BASE}/${repo}/blobs/${digest}" | \
jq -r '.created'
}
# 函数:删除指定镜像(按digest删除,避免软连接问题)
delete_image() {
local repo="$1"
local digest="$2"
echo -e "${RED}删除镜像:${repo}@${digest}${NC}"
curl -s -X DELETE -u "${REGISTRY_USER}:${REGISTRY_PASS}" \
"https://${REGISTRY}/${NEXUS_BASE}/${repo}/manifests/${digest}"
}
# 主清理逻辑
main() {
echo "========== 开始清理旧镜像 =========="
echo "仓库地址:${REGISTRY}"
echo "保留天数:${RETENTION_DAYS}"
echo "保留最新标签数:${KEEP_LATEST}"
# 获取所有仓库
repos=$(get_repositories)
for repo in $repos; do
echo -e "\n正在处理仓库:${GREEN}${repo}${NC}"
# 获取所有标签(按字母排序)
tags=($(get_tags "$repo" | sort -r))
# 提取需要保留的标签(最新N个)
keep_tags=("${tags[@]:0:$KEEP_LATEST}")
echo "保留的最新的${KEEP_LATEST}个标签:${keep_tags[*]}"
# 过滤需要清理的标签(排除保留标签)
for tag in "${tags[@]:$KEEP_LATEST}"; do
# 获取镜像创建时间(ISO 8601格式)
created=$(get_image_creation_time "$repo" "$tag")
if [ -z "$created" ]; then
echo "跳过无法获取时间的镜像:${repo}:${tag}"
continue
fi
# 计算时间差(秒)
created_epoch=$(date -d "$created" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "$created" +%s 2>/dev/null)
now_epoch=$(date +%s)
diff_days=$(( (now_epoch - created_epoch) / 86400 ))
echo "镜像 ${repo}:${tag} 创建于 ${created},距今 ${diff_days} 天"
# 如果超过保留天数,则删除
if [ $diff_days -gt $RETENTION_DAYS ]; then
# 获取digest(用于安全删除)
digest=$(curl -s -u "${REGISTRY_USER}:${REGISTRY_PASS}" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
"https://${REGISTRY}/${NEXUS_BASE}/${repo}/manifests/${tag}" | \
jq -r '.config.digest')
delete_image "$repo" "$digest"
else
echo -e "${GREEN}保留镜像:${repo}:${tag}${NC}"
fi
done
done
# 执行垃圾回收(可选,需仓库支持)
echo -e "\n正在执行垃圾回收..."
curl -s -X POST -u "${REGISTRY_USER}:${REGISTRY_PASS}" \
"https://${REGISTRY}/${NEXUS_BASE}/_gc" || echo "垃圾回收失败(部分仓库不支持手动触发)"
echo -e "${GREEN}========== 清理完成 ==========${NC}"
}
# 执行
main
设置定时任务(crontab)
# 每晚凌晨2点执行清理,保留7天 0 2 * * * /opt/scripts/docker_cleanup.sh registry.example.com 7 >> /var/log/docker_cleanup.log 2>&1
常见问题解答
Q1:脚本执行后提示“证书验证失败”怎么办?
A:自建仓库使用自签名证书时,需添加 -k 参数跳过验证,或在Docker配置中信任该证书:
# 方式一:临时跳过 curl -k -s -u "user:pass" "https://registry:5000/v2/_catalog" # 方式二:永久信任 cp ca.crt /etc/docker/certs.d/registry:5000/ca.crt systemctl restart docker
Q2:删除镜像后仓库空间没有释放?
A:Docker Registry v2采用分层存储,删除manifest只删除引用,实际层数据需通过垃圾回收(Garbage Collection)释放,执行:
# 在registry容器内执行 docker exec -it registry bin/registry garbage-collect /etc/docker/registry/config.yml
注意:垃圾回收期间会锁住仓库,需在低峰期执行。
Q3:如何只删除特定前缀的标签?
A:在脚本的标签过滤逻辑中加入正则匹配:
# 删除以 "dev-" 或 "nightly-" 开头的标签
if [[ "$tag" =~ ^(dev|nightly)- ]]; then
delete_image "$repo" "$digest"
fi
Q4:脚本执行效率低,如何处理成百上千个镜像?
A:优化方向:
- 使用并发处理(
xargs -P或GNU parallel) - 批量获取所有标签的创建时间(调用仓库的
_catalogAPI一次获取) - 对于超过1000个镜像的仓库,考虑使用仓库原生API的过滤参数(如Harbor的
?q=created:<=2023-01-01)
最佳实践建议
-
先模拟再执行:在脚本中添加
--dry-run模式,仅输出将要删除的镜像,不实际执行删除。if [ "$DRY_RUN" = "true" ]; then echo "待删除:${repo}:${tag}" else delete_image "$repo" "$digest" fi -
日志审计:记录每次删除操作的镜像名、时间戳和操作人员。
echo "$(date) - 删除 ${repo}:${tag} (digest:${digest})" >> /var/log/cleanup_audit.log -
保留关键标签保护:在脚本中加入白名单,防止误删
latest、stable等关键标签。PROTECTED_TAGS=("latest" "stable" "production") if [[ " ${PROTECTED_TAGS[@]} " =~ " ${tag} " ]]; then echo -e "${GREEN}保护标签,跳过:${tag}${NC}" continue fi -
监控存储趋势:结合Prometheus和Grafana监控仓库存储使用率,设置自动清理阈值(如超过80%容量触发清理)。
-
多仓库统一管理:对于多个镜像仓库(如Harbor、Nexus、AWS ECR),可编写wrapper脚本统一调用不同仓库的API。