nexus版本为:3.18.1-01 地址是:http://192.168.54.140:8081 用户是:admin 密码是: 123 仓库名: docker-repo
nexus的web页面可以配置定时清理策略,然后,这里只能指定清理多少天之前的镜像,或最后一次下载时间早于指定天数前的镜像,这不符合我们的要求。
image.png
我们的清理策略是:自动清理apron-repo仓库中所有镜像,每个镜像只保留最新的5个标签版本,并记录详细日志
先建一个task,在“System”->“Tasks”下创建定时任务,选择“Admin - Compact blob store”任务 ,名称为:cleanStore,每天03:00时运行
image.png
再建一个脚本,用来实现我们的清理策略。
$ vi /root/nexus_cleanup.sh
#!/bin/bash
# Nexus3 Docker镜像自动清理脚本(带日志记录)
# 功能:自动清理apron-repo仓库中所有镜像,每个镜像只保留最新的5个标签版本,并记录详细日志
# 配置区 ==============================================
NEXUS_URL="http://192.168.54.140:8081" # Nexus地址
USERNAME="admin" # 用户名
PASSWORD="123" # 密码
REPOSITORY="docker-repo" # 仓库名
KEEP_LATEST=5 # 保留最新标签数量
LOG_FILE="/tmp/nexusClean.log" # 日志文件路径
COMPACT_BLOB_STORE=true #压缩仓库
TASK_NAME="cleanStore" #压缩仓库任务名称
IMAGE_NAMES=""
CONTINUATION_TOKEN=""
# =====================================================
# 日志函数 ===========================================
function log() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case $level in
"INFO") echo -e "$timestamp [\033[32mINFO\033[0m] $message" | tee -a $LOG_FILE ;;
"WARNING") echo -e "$timestamp [\033[33mWARNING\033[0m] $message" | tee -a $LOG_FILE ;;
"ERROR") echo -e "$timestamp [\033[31mERROR\033[0m] $message" | tee -a $LOG_FILE ;;
*) echo -e "$timestamp [UNKNOWN] $message" | tee -a $LOG_FILE ;;
esac
}
# 日志文件自维护(大于5M则截取最后50行)
[ `du -m /tmp/nexusClean.log | awk '{print $1}'` -gt 5 ] && tail -50 /tmp/nexusClean.log>/tmp/nexusClean-tmp && mv /tmp/nexusClean-tmp /tmp/nexusClean.log -f
# 初始化日志文件 =====================================
echo "===== Nexus清理脚本执行日志 =====" >> $LOG_FILE
log "INFO" "脚本启动时间: $(date '+%Y-%m-%d %H:%M:%S')"
log "INFO" "Nexus地址: $NEXUS_URL"
log "INFO" "目标仓库: $REPOSITORY"
log "INFO" "保留最新标签数: $KEEP_LATEST"
# =====================================================
# 函数定义 ===========================================
function get_all_images() {
log "INFO" "正在获取仓库 $REPOSITORY 中的所有镜像..."
# 获取所有镜像名称(去重)
# 循环处理分页(兼容初始空值和后续的"null")
while [ "$CONTINUATION_TOKEN" != "null" ] && { [ -z "$CONTINUATION_TOKEN" ] || [ "$CONTINUATION_TOKEN" != "null" ]; }; do
# 构造API请求URL(首次请求不带continuationToken参数)
if [ -z "$CONTINUATION_TOKEN" ]; then
API_URL="$NEXUS_URL/service/rest/v1/search?repository=$REPOSITORY"
else
API_URL="$NEXUS_URL/service/rest/v1/search?repository=$REPOSITORY&continuationToken=$CONTINUATION_TOKEN"
fi
# 发送请求并处理响应
API_RESPONSE=$(curl -s -u "$USERNAME:$PASSWORD" -X GET "$API_URL" -H "accept:application/json")
# 追加当前页结果
PAGE_NAMES=$(echo "$API_RESPONSE" | jq -r '.items[].name' | sort | uniq)
IMAGE_NAMES+=$'\n'"$PAGE_NAMES"
# 获取下一页token(处理jq返回的"null"字符串)
CONTINUATION_TOKEN=$(echo "$API_RESPONSE" | jq -r '.continuationToken // empty')
if [ -z "$CONTINUATION_TOKEN" ]; then
CONTINUATION_TOKEN="null" # 无后续分页时显式设置为"null"
fi
done
IMAGE_NAMES=$(echo "$IMAGE_NAMES" | sort | uniq)
if [ -z "$IMAGE_NAMES" ]; then
log "ERROR" "未获取到任何镜像信息"
exit 1
fi
log "INFO" "找到以下镜像:\n$IMAGE_NAMES"
}
function cleanup_image_tags() {
local image_name=$1
log "INFO" "开始处理镜像: $image_name"
# 获取该镜像所有标签(按创建时间倒序排序)
TAGS=$(curl -s -u "$USERNAME:$PASSWORD" -X GET \
"$NEXUS_URL/service/rest/v1/search?repository=$REPOSITORY&name=$image_name" \
-H "accept:application/json" | jq -r '.items | sort_by(.created) | reverse | .[].version')
TOTAL_TAGS=$(echo "$TAGS" | wc -l)
log "INFO" "镜像 $image_name 共有 $TOTAL_TAGS 个标签"
if [ $TOTAL_TAGS -le $KEEP_LATEST ]; then
log "INFO" "标签数量未超过保留限制($KEEP_LATEST),跳过处理"
return
fi
# 计算需要删除的标签数量
TAGS_TO_DELETE=$((TOTAL_TAGS - KEEP_LATEST))
log "INFO" "需要删除 $TAGS_TO_DELETE 个旧标签"
# 获取要删除的标签列表(跳过前KEEP_LATEST个)
DELETE_TAGS=$(echo "$TAGS" | tail -n +$((KEEP_LATEST + 1)))
# 逐个删除旧标签
for tag in $DELETE_TAGS; do
log "INFO" "正在删除标签: $tag"
# 获取组件ID
COMPONENT_ID=$(curl -s -u "$USERNAME:$PASSWORD" -X GET \
"$NEXUS_URL/service/rest/v1/search?repository=$REPOSITORY&name=$image_name&version=$tag" \
-H "accept:application/json" | jq -r '.items[0].id')
if [ -z "$COMPONENT_ID" ] || [ "$COMPONENT_ID" == "null" ]; then
log "WARNING" "未找到 $image_name:$tag 的组件ID"
continue
fi
# 执行删除
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -u "$USERNAME:$PASSWORD" -X DELETE \
"$NEXUS_URL/service/rest/v1/components/$COMPONENT_ID" \
-H "accept:application/json")
if [ "$RESPONSE" -eq 204 ]; then
log "INFO" "成功删除 $image_name:$tag (组件ID: $COMPONENT_ID)"
else
log "ERROR" "删除失败,HTTP状态码: $RESPONSE"
fi
done
}
function compact_blob_store() {
log "INFO" "正在触发Blob存储压缩任务..."
# 1. 获取已存在任务的ID
TASK_ID=$(curl -s -u "$USERNAME:$PASSWORD" \
"$NEXUS_URL/service/rest/v1/tasks" \
-H "accept: application/json" | \
jq -r --arg TASK_NAME "$TASK_NAME" '.items[] | select(.name==$TASK_NAME) | .id')
CURRENT_STATE=$(echo "$TASK_INFO" | jq -r '.currentState')
# 2. 检查任务是否存在
if [ -z "$TASK_ID" ]; then
log "ERROR" "未找到名为'$TASK_NAME'的任务,请先在UI创建"
return 1
fi
# 3. 触发任务执行(如果任务未在运行)
if [ "$CURRENT_STATE" != "RUNNING" ]; then
RUN_RESULT=$(curl -s -o /dev/null -w "%{http_code}" -u "$USERNAME:$PASSWORD" -X POST \
"$NEXUS_URL/service/rest/v1/tasks/$TASK_ID/run")
if [ "$RUN_RESULT" -eq 204 ]; then
log "INFO" "任务触发成功 (ID: $TASK_ID),等待完成..."
else
log "ERROR" "任务触发失败,HTTP状态码: $RUN_RESULT"
return 1
fi
else
log "WARN" "任务已在运行中 (ID: $TASK_ID)"
fi
}
# =====================================================
# 主执行流程 ==========================================
# 1. 获取所有镜像名称
get_all_images
# 2. 对每个镜像执行清理
for image in $IMAGE_NAMES; do
cleanup_image_tags "$image"
done
# 3. 可选清理操作
if [ "$COMPACT_BLOB_STORE" = true ]; then
compact_blob_store
fi
log "INFO" "所有操作已完成!"
log "INFO" "脚本结束时间: $(date '+%Y-%m-%d %H:%M:%S')"
# =====================================================
$ chmod +x /root/nexus_cleanup.sh
说明:
- 为什么在清理镜像后要进行压缩:因为只删除镜像它只是为镜像打上delete标记,并不会真正删除
- 当前nexus所支持的api调用,可以在System--API菜单下找到,并测试,也可以在命令行测试:
curl -v -u admin:123 "http://192.168.54.140:8081/service/rest/v1/tasks"
* Trying 192.168.54.140...
* TCP_NODELAY set
* Connected to 192.168.54.140 (192.168.54.140) port 8081 (#0)
* Server auth using Basic with user 'admin'
> GET /service/rest/v1/tasks HTTP/1.1
> Host: 192.168.54.140:8081
> Authorization: Basic YWRtaW46c3pjYWFjfjE=
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 17 Jun 2025 08:45:47 GMT
< Server: Nexus/3.18.1-01 (OSS)
< X-Content-Type-Options: nosniff
< Content-Type: application/json
< Content-Length: 693
<
{
"items" : [ {
"id" : "194ab8d3-b40d-4837-a248-77db54e04f86",
"name" : "Cleanup service",
"type" : "repository.cleanup",
"message" : "Run repository cleanup",
"currentState" : "WAITING",
"lastRunResult" : "OK",
"nextRun" : "2025-06-18T01:00:00.000+0000",
"lastRun" : "2025-06-17T01:00:00.004+0000"
}, {
"id" : "abebd256-fe95-4ee2-a2f3-7b769ba4e5c9",
"name" : "cleanStore",
"type" : "blobstore.compact",
"message" : "Compacting default blob store",
"currentState" : "WAITING",
"lastRunResult" : "OK",
"nextRun" : "2025-06-17T19:00:00.000+0000",
"lastRun" : "2025-06-17T08:19:20.597+0000"
} ],
"continuationToken" : null
* Connection #0 to host 192.168.54.140 left intact
可以看到脚本中删除镜像或调用任务时,都需要先找到它们的id,然后再以id来调用,系统并不支持直接以name来调用
这里docker镜像的标准是yyyymmddHHmmss格式,以便于排序
再将以上脚本配置为定时任务(每周日凌晨2点执行,UTC时间 )
$ (crontab -l | grep "$cron_job"; echo "0 18 * * 0 /root/nexus_cleanup.sh >/dev/null 2>&1") | crontab -
no crontab for root
$ crontab -l # 查看一下
0 18 * * 0 /root/nexus_cleanup.sh >/dev/null 2>&1
手工测试一下脚本
$ /root/nexus_cleanup.sh
2025-06-17 08:58:16 [INFO] 脚本启动时间: 2025-06-17 08:58:16
2025-06-17 08:58:16 [INFO] Nexus地址: http://192.168.54.140:8081
2025-06-17 08:58:16 [INFO] 目标仓库: apron-repo
2025-06-17 08:58:16 [INFO] 保留最新标签数: 5
2025-06-17 08:58:16 [INFO] 正在获取仓库 apron-repo 中的所有镜像...
2025-06-17 08:58:16 [INFO] 找到以下镜像:
app1
dev/app1
helloworld
kuboard
nexus3
2025-06-17 08:58:16 [INFO] 开始处理镜像: app1
2025-06-17 08:58:16 [INFO] 镜像 app1 共有 4 个标签
2025-06-17 08:58:16 [INFO] 标签数量未超过保留限制(5),跳过处理
2025-06-17 08:58:16 [INFO] 开始处理镜像: dev/app1
2025-06-17 08:58:16 [INFO] 镜像 dev/app1 共有 5 个标签
2025-06-17 08:58:16 [INFO] 标签数量未超过保留限制(5),跳过处理
2025-06-17 08:58:16 [INFO] 开始处理镜像: helloworld
2025-06-17 08:58:16 [INFO] 镜像 helloworld 共有 6 个标签
2025-06-17 08:58:16 [INFO] 需要删除 1 个旧标签
2025-06-17 08:58:16 [INFO] 正在删除标签: 202506140429
2025-06-17 08:58:17 [INFO] 成功删除 helloworld:202506140429 (组件ID: YXByb24tcmVwbzpkZmJlZjA5ZWZlMTY0NGVhZjA2OTYxMmI1MTg5NjRkMQ)
2025-06-17 08:58:17 [INFO] 开始处理镜像: kuboard
2025-06-17 08:58:17 [INFO] 镜像 kuboard 共有 1 个标签
2025-06-17 08:58:17 [INFO] 标签数量未超过保留限制(5),跳过处理
2025-06-17 08:58:17 [INFO] 开始处理镜像: nexus3
2025-06-17 08:58:17 [INFO] 镜像 nexus3 共有 1 个标签
2025-06-17 08:58:17 [INFO] 标签数量未超过保留限制(5),跳过处理
2025-06-17 08:58:17 [INFO] 正在触发Blob存储压缩任务...
2025-06-17 08:58:17 [INFO] 任务触发成功 (ID: abebd256-fe95-4ee2-a2f3-7b769ba4e5c9),等待完成...
2025-06-17 08:58:17 [INFO] 所有操作已完成!
2025-06-17 08:58:17 [INFO] 脚本结束时间: 2025-06-17 08:58:17