ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker磁盘空间不足?自动化清理脚本全攻略(容器/镜像/卷/缓存)

Docker磁盘空间不足?自动化清理脚本全攻略(容器/镜像/卷/缓存) 周一早会还没开完运维同事就在群里扔了张监控截图开发机根分区使用率 93%再过半天就要告警。登上那台机器一看/var/lib/docker占了快 150GB里面躺着几十个none镜像、不知道停了多少天的退出容器、以及一堆早就没人用的旧镜像。这种场景在大规模使用 Docker 做日常开发和测试的团队里太常见了。问题很简单镜像、容器、卷、构建缓存会以你完全感知不到的速度把磁盘填满。手动清理能顶一阵子但过两周又满。我后来直接写了一个自动化的 Docker 容器与镜像清理脚本配合定时任务跑起来再也没为这事被叫醒过。这篇文章就把整套方案拆开讲清理对象怎么分层、保留策略怎么定、核心脚本怎么写、怎么用 cron 和 systemd timer 让它自动跑以及我在真实环境里踩过的坑。1. 一个项目被什么逼出来的Docker 磁盘膨胀的根源1.1 先看看磁盘空间都去哪了Docker 的磁盘占用并不是单纯“容器文件”这一个概念它由好几块组成镜像层Image Layers每个 Dockerfile 指令都会产生一层同一台机器上多个镜像之间会共享底层但如果你心大从来不清理每 build 一次留下的旧层会越积越多。容器层Container Layers容器被创建后即使已经exit它的可写层和日志文件仍然留在磁盘上。容器一多这些退出容器就是隐形磁盘杀手。数据卷Volumes挂载到容器里的数据卷只要不被容器引用就是「悬空卷」日积月累同样很占空间。构建缓存Build Cache每次docker build都会生成层缓存CI 上跑大量构建的话这部分量级可能比你想象的还要大。在动手写清理脚本之前我建议你先在宿主机上跑一下这两条命令搞清楚现状df -h docker system dfdocker system df会输出类似下面的内容TYPE TOTAL ACTIVE SIZE RECLAIMABLE Images 128 21 72.34GB 45.6GB (63%) Containers 96 14 18.56GB 17.3GB (93%) Local Volumes 47 19 9.812GB 8.4GB (85%) Build Cache 132 0 21.44GB 21.44GB (100%)这张表一出来你就明白了真正活跃资源只占一小部分剩余全是可回收垃圾。所谓“自动化清理脚本”本质上就是把表中RECLAIMABLE那一列定期清掉一部分同时不误伤还在用的资源。1.2 手动清理的三个痛点很多人遇到磁盘告警后的第一反应是手动执行docker system prune -af这条命令确实删得快但它有两个我特别不喜欢的问题。第一-a会把所有没有被容器引用的镜像全删掉哪怕你昨天刚 pull 下来、今天想用的旧版本也被清掉了。开发机还好说最多重新拉取生产或预发环境这样操作就是事故级别的“删库”。第二手动清理不可持续。你这次清完 50GB团队里只要有人继续无脑docker build两周后告警又来了。所谓自动化不只是写几个prune命令而是要把清理频率、保留策略、日志记录、并发保护都设计进去让它变成一套每天自动巡逻的机制而不是一次性的急救操作。1.3 自动化清理脚本的目标边界写这个脚本之前我先给自己定了几个目标防止越写越复杂只清理确定可回收的资源退出容器、悬空镜像、悬空卷、构建缓存这些是清理主目标。必须有保留策略不能说“全部删除”得允许业务方指定保留最近多少天的镜像和容器。必须可预测、可试跑脚本在正式部署前必须能 dry-run先看它会删什么再决定是否真正执行。避免误删业务数据有名字的数据卷、通过白名单标记的资源在脚本里默认不清理。结果可观测每次执行要有日志能统计释放空间量方便以后排查问题。说白了这个脚本不是越“莽”越好而是在“清理效果”和“安全边界”之间找一个团队能接受的平衡点。接下来的每一节都在解释我为什么这么设计。2. 清理策略与方案设计先定规则再写代码2.1 清理对象的三个层次我把清理任务拆成了四个递进层级每层单独做、单独记日志第一层已经退出的容器。容器exit之后日志和可写层还留着。这类容器没有使用价值删除后运行数据不会受影响除非当初数据没落盘。第二层悬空镜像与未使用镜像。悬空镜像是none:none这种是历史 build 留下的层引用碎片删掉没有任何副作用。未使用镜像指有 tag 但当前没有任何容器引用的镜像删的时候需要非常谨慎。第三层悬空数据卷。项目重命名、Compose 服务删了之后数据卷往往留在原地。这类卷体积往往不小而且默认不在docker volume ls里直接标明显眼的删除提示很多人根本不知道怎么清。第四层构建缓存。CI 或本地频繁 build 后Build Cache的占用量会快速增长经常有几十 GB。它理论上可以重建所以保留策略可以相对激进。分层的好处是我可以分别控制每一层的开关和保留时间。比如生产环境可以把“未使用镜像”的清理关掉只保留悬空镜像清理开发机则可以全面推开。2.2 保留策略怎么定时间窗口 状态过滤 白名单我的脚本里核心参数就这几个参数含义我的默认推荐值设置逻辑KEEP_CONTAINER_DAYS退出容器保留天数2容器退出超过 2 天基本不会再用日志也没保留价值KEEP_IMAGE_DAYS未使用镜像保留天数7给发布回滚窗口留足够时间KEEP_VOLUME_DAYS悬空卷保留天数7防止有人在卷里放过临时备份DISK_THRESHOLD触发清理的磁盘使用率阈值80低于阈值时跳过避免每天无谓操作CLEAN_BUILD_CACHE是否清理构建缓存true缓存重建成本低清理收益大时间窗口的逻辑是删除创建时间早于指定天数的资源。这里要特别说清楚一个容易踩坑的点docker container prune --filter until...里until过滤的是容器创建时间不是“退出时间”。如果一台容器创建了但一直没启动、没过几秒就退出单靠until是没法精准按“停止时间”删的。所以我脚本里第一层的容器清理额外对FinishedAt做了判断用容器实际退出时间来做比较这个逻辑在后面代码里有具体实现。除了时间窗口白名单策略也很重要。我给脚本留了一个黑名单/白名单接口镜像白名单例如registry.internal.base/*这类基础镜像任何情况下都不参与清理。容器黑名单例如跑了数据库的容器即使处于 exited 状态也要保留一段时间。规则上宁可保守一点。清理是“锦上添花”不是“救火”没必要为了多释放 2GB 把别人正在参考的容器删了。2.3 为什么用 Shell Cron/Systemd Timer而不是再上个框架我知道现在很多运维体系已经上了 Ansible、Kubernetes、各种自动化运维平台。那为什么这个场景我还是坚持用 Shell 脚本原因很简单清理 Docker 资源这件事的核心操作就是dockerCLI 加几条命令Shell 已经足够表达且部署依赖最小。一台没有装 Python、没有装 Ansible 的裸 Linux 服务器只要有 bash 和 Docker脚本就能跑。这基本覆盖了 99% 的宿主机。而调度层面cron 和 systemd timer 都是操作系统原生能力不引入额外组件也不占多少资源。越是这种“小而通用”的运维工具越值得保持轻量。如果你正好在用 Ansible 管理这批机器当然可以把脚本放到 Playbook 里分发但这和脚本本身不冲突——它是被分发的那个“动作”不是整个自动化底座。2.4 脚本设计的四个关键原则写这个清理脚本时我给自己定了几条必须遵守的底线后面代码全是围绕这几条展开的幂等性脚本可以重复执行多次每次结果一致不会因为上次删到一半导致下次报错。防重入如果上一次清理任务还没跑完下一次定时触发就直接跳过避免两个任务并发冲突。可观测每次清理前后都要记录磁盘占用脚本执行要落日志方便回溯“上次到底删了啥”。可演练默认支持 dry-run 模式输出将执行的命令但不真正执行上线前先在测试机器上看效果。有了这些原则脚本从“能用”变成“敢用”。下面开始讲代码实现。3. 脚本实现核心逻辑与完整代码3.1 前置检查与锁机制脚本启动后先做三件事检查 Docker 是否可用、确认磁盘使用率是否达到阈值、用flock加锁防止并发执行。为什么先做检查因为 cron 环境跟交互式终端不一样PATH 可能不包含/usr/bindocker命令可能找不到。如果直接把清理命令抛出去你会看到一长串command not found而且清理完全没执行还容易让人误以为清理成功了。这段前置逻辑我放在脚本开头也是所有后续操作的安全壳#!/usr/bin/env bash set -euo pipefail LOG_FILE/var/log/docker-cleanup.log LOCK_FILE/tmp/docker-cleanup.lock DRY_RUN${DRY_RUN:-false} log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* | tee -a $LOG_FILE } exec 9$LOCK_FILE if ! flock -n 9; then log 已有清理任务在执行本次跳过 exit 0 fi if ! command -v docker /dev/null 21; then log 错误docker 命令不存在请检查 PATH 或安装 Docker exit 1 fi if ! docker info /dev/null 21; then log 错误无法连接 Docker Daemon请检查服务状态 exit 1 fi这里最容易被忽视的是set -euo pipefail。没有它管道中间某步失败脚本可能继续执行下去导致后续清理动作基于错误状态操作非常危险。加了这个选项后任意命令非零退出都会让脚本停下至少不会“带病运行”。3.2 第一层已退出容器的筛选与清理容器清理是四个层级中最容易误伤的一层所以我的处理方式不是直接docker container prune --force而是先拿到退出容器列表再逐个检查退出时间。get_exited_containers() { docker ps -a --filter statusexited --format {{.ID}}\t{{.Image}}\t{{.State}}\t{{.FinishedAt}} } clean_containers() { local keep_days$1 local now_ts now_ts$(date %s) local keep_ts$((now_ts - keep_days * 24 * 3600)) while IFS$\t read -r cid image state finished_at; do [[ -z $cid ]] continue local finished_ts finished_ts$(date -d $finished_at %s) if [[ -n $finished_ts $finished_ts -lt $keep_ts ]]; then log 删除已退出容器 $cid (镜像 $image, 退出时间 $finished_at) if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker rm $cid else docker rm $cid /dev/null 21 || log 删除容器 $cid 失败 fi fi done (get_exited_containers) }这段代码解决了until过滤器基于创建时间的问题docker inspect里的FinishedAt代表容器实际退出时间用它才能精准做到“退出超过 2 天就删除”。另外Container 正在被使用的情况天然不会出现在statusexited列表里所以这个函数安全。3.3 第二层镜像清理的三种粒度镜像清理我认为至少要提供三种模式不能一把梭。默认我用docker image prune不加-a只清理悬空镜像这部分镜像没有任何标签删除后对应用无影响。第二种模式加-a把所有未被容器引用的镜像也清掉。第三种是带白名单的高级模式从镜像仓库地址上做过滤。代码我做成一个函数clean_images() { local keep_days$1 local prune_all${2:-false} if [[ $prune_all true ]]; then log 清理所有未被容器引用的镜像保留 $keep_days 天 if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker image prune -af --filter until${keep_days}h else docker image prune -af --filter until${keep_days}h fi else log 清理悬空镜像保留 $keep_days 天 if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker image prune -f --filter until${keep_days}h else docker image prune -f --filter until${keep_days}h fi fi }这里有个我踩过、想特别提醒你的细节docker image prune -a的-a会删除所有 tag 未被容器引用的镜像。假设你本地拉了一个redis:7.0但当前没有容器用它它会被删掉。等你要用的时候只能重新拉取。如果这台机器是公司的预发环境、带宽又紧张这个成本会非常难看。所以我强烈建议第一版脚本默认只开悬空镜像清理跑两周观察后再决定要不要开-a模式。另外until过滤器对镜像的意义是“只删除创建时间早于指定时间戳”的镜像。它的实现基于镜像元数据不是基于“上次使用时间”所以你对某些“最近才被 pull 但一直没运行”的镜像要有心理预期它们也可能在-a模式下被清掉。3.4 第三层数据卷与构建缓存数据卷清理是这个脚本里最需要小心的一部分因为卷里的数据可能没有写在镜像层里。所谓悬空卷是指没有任何容器引用的 volume这类卷删掉之后数据基本无法找回。我的策略是把卷清理独立成开关默认不启用需要显式设置CLEAN_VOLUMEStrue才会执行。clean_volumes() { if [[ ${CLEAN_VOLUMES:-false} ! true ]]; then log 卷清理已关闭跳过 return fi log 清理悬空数据卷保留 $1 天 if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker volume prune -f --filter until${1}h else docker volume prune -f --filter until${1}h fi } clean_build_cache() { if [[ ${CLEAN_BUILD_CACHE:-true} ! true ]]; then log 构建缓存清理已关闭跳过 return fi log 清理构建缓存保留 $1 天 if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker builder prune -f --filter until${1}h else docker builder prune -f --filter until${1}h fi }卷清理我为什么默认关闭因为我见过有同事把 mysql 的数据目录直接用-v挂在匿名卷上容器删了卷还在但他以为数据已经没了。如果脚本自动把匿名卷清理掉那就真的把最后一份数据也删没了。有名字的卷和docker compose管理的带 label 卷在docker volume prune下也会被清理只要没有容器引用所以更得谨慎。构建缓存这一点倒是可以放开清。CI 频率高的机器上构建缓存动辄几十 GB但它本质上是可以重算的中间产物删除对运行中的容器没有任何影响。我在实际环境中把CLEAN_BUILD_CACHE默认设成true一是收益大二是风险低。3.5 日志轮转与磁盘容量统计清理过程要可观测所以我每次执行前先记录一次容量执行后再记录一次形成对比。日志本身也需要管理否则一个日志文件会无限膨胀反过来又把刚释放的磁盘吃回去。日志轮转我直接在脚本里用logrotate打理/var/log/docker-cleanup.log { daily rotate 7 compress missingok copytruncate }容量统计我用docker system df --format来拿总量。部分旧版本 Docker 不支持这个参数所以我做了兼容get_docker_dir_usage() { du -sh /var/lib/docker 2/dev/null | awk {print $1} }统计后写入日志log 清理前 Docker 目录占用$(get_docker_dir_usage) clean_containers $KEEP_CONTAINER_DAYS clean_images $KEEP_IMAGE_DAYS ${PRUNE_ALL_IMAGES:-false} clean_volumes $KEEP_VOLUME_DAYS clean_build_cache $KEEP_IMAGE_DAYS log 清理后 Docker 目录占用$(get_docker_dir_usage)3.6 完整脚本参考以下是一个可以直接上测试机试跑的版本我把它放在/usr/local/sbin/docker-cleanup.sh#!/usr/bin/env bash set -euo pipefail LOG_FILE/var/log/docker-cleanup.log LOCK_FILE/tmp/docker-cleanup.lock # 可调参数 KEEP_CONTAINER_DAYS${KEEP_CONTAINER_DAYS:-2} KEEP_IMAGE_DAYS${KEEP_IMAGE_DAYS:-7} KEEP_VOLUME_DAYS${KEEP_VOLUME_DAYS:-7} DISK_THRESHOLD${DISK_THRESHOLD:-80} PRUNE_ALL_IMAGES${PRUNE_ALL_IMAGES:-false} CLEAN_VOLUMES${CLEAN_VOLUMES:-false} CLEAN_BUILD_CACHE${CLEAN_BUILD_CACHE:-true} DRY_RUN${DRY_RUN:-false} log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* | tee -a $LOG_FILE } exec 9$LOCK_FILE if ! flock -n 9; then log 已有清理任务在执行本次跳过 exit 0 fi if ! command -v docker /dev/null 21; then log 错误docker 命令不存在 exit 1 fi if ! docker info /dev/null 21; then log 错误无法连接 Docker Daemon exit 1 fi disk_usage$(df -P /var/lib/docker 2/dev/null | awk NR2 {print $5} | sed s/%//) if [[ -z $disk_usage ]]; then disk_usage$(df -P / | awk NR2 {print $5} | sed s/%//) fi log 当前磁盘使用率: ${disk_usage}% if (( disk_usage DISK_THRESHOLD )); then log 磁盘使用率低于阈值 ${DISK_THRESHOLD}%本次跳过 exit 0 fi get_exited_containers() { docker ps -a --filter statusexited --format {{.ID}}\t{{.Image}}\t{{.FinishedAt}} } clean_containers() { local keep_days$1 local now_ts keep_ts now_ts$(date %s) keep_ts$((now_ts - keep_days * 24 * 3600)) while IFS$\t read -r cid image finished_at; do [[ -z $cid ]] continue finished_ts$(date -d $finished_at %s 2/dev/null || echo 0) if [[ -n $finished_ts $finished_ts -lt $keep_ts ]]; then log 删除已退出容器 $cid (镜像 $image, 退出时间 $finished_at) if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker rm $cid else docker rm $cid /dev/null 21 || log 删除容器 $cid 失败 fi fi done (get_exited_containers) } clean_images() { local keep_days$1 if [[ $PRUNE_ALL_IMAGES true ]]; then log 清理所有未被容器引用的镜像保留 $keep_days 天 if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker image prune -af --filter until${keep_days}h else docker image prune -af --filter until${keep_days}h fi else log 清理悬空镜像保留 $keep_days 天 if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker image prune -f --filter until${keep_days}h else docker image prune -f --filter until${keep_days}h fi fi } clean_volumes() { if [[ $CLEAN_VOLUMES ! true ]]; then log 卷清理未开启跳过 return fi log 清理悬空数据卷保留 $1 天 if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker volume prune -f --filter until${1}h else docker volume prune -f --filter until${1}h fi } clean_build_cache() { if [[ $CLEAN_BUILD_CACHE ! true ]]; then log 构建缓存清理未开启跳过 return fi log 清理构建缓存保留 $1 天 if [[ $DRY_RUN true ]]; then echo [DRY RUN] docker builder prune -f --filter until${1}h else docker builder prune -f --filter until${1}h fi } log Docker 自动清理开始 log 清理前 Docker 目录占用: $(du -sh /var/lib/docker 2/dev/null | awk {print $1}) clean_containers $KEEP_CONTAINER_DAYS clean_images $KEEP_IMAGE_DAYS clean_volumes $KEEP_VOLUME_DAYS clean_build_cache $KEEP_IMAGE_DAYS log 清理后 Docker 目录占用: $(du -sh /var/lib/docker 2/dev/null | awk {print $1}) log Docker 自动清理结束 脚本先不要急着上生产DRY_RUNtrue跑一遍看日志里将要删哪些东西确认无误后再关闭 dry-run。4. 部署与自动化调度让它自己跑起来4.1 第一种调度方式cron最简单的部署方式就是在宿主机上添加 cron 任务。我通常把清理时间放在凌晨 2 点到 4 点之间避开开发和 CI 的高峰期。crontab -e加入一行0 2 * * * /usr/local/sbin/docker-cleanup.sh /var/log/docker-cleanup.log 21这行的意思是每天凌晨 2 点整执行一次。如果你不想让脚本在执行时把日志写两份可以去掉 /var/log/docker-cleanup.log 21让脚本自己用tee写。不过加上这行有个好处即使脚本因为意外或语法错误根本没有执行到log()函数cron 也会把报错记录到日志文件里排查起来方便很多。这里要特别提醒一个 cron 环境常见问题cron 执行时的 PATH 很精简通常只有/usr/bin:/bindocker 如果装在/usr/local/bin下脚本里command -v docker可能直接失败。我在脚本里已经做了检查如果报错会明确告诉你看 PATH。你也可以在 crontab 顶部加一行PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin4.2 第二种调度方式systemd Timer如果你的宿主机用的是 systemd现在主流 Linux 发行版基本都是我更推荐用 systemd timer 而非 cron。它自带日志集中管理还支持失败重试等更精细的行为是更规范的现代方案。创建服务单元/etc/systemd/system/docker-cleanup.service[Unit] DescriptionDocker Cleanup Service Afterdocker.service Requiresdocker.service [Service] Typeoneshot ExecStart/usr/local/sbin/docker-cleanup.sh StandardOutputjournal StandardErrorjournal创建计时器单元/etc/systemd/system/docker-cleanup.timer[Unit] DescriptionRun Docker Cleanup Daily [Timer] OnCalendar*-*-* 02:30:00 Persistenttrue [Install] WantedBytimers.target启用并启动systemctl daemon-reload systemctl enable --now docker-cleanup.timerPersistenttrue的意思是如果到了计划时间机器正好关机下次开机后会自动补跑一次这对长期关机的开发机或者夜间节能的服务器很有用。想看执行情况和日志就输入systemctl status docker-cleanup.service journalctl -u docker-cleanup.service -n 504.3 与 CI/CD 环境集成如果你是在 Jenkins 或 GitLab Runner 机器上跑构建清理脚本的集成思路会略有不同。CI Runner 机器的特点是构建非常频繁镜像堆积速度远高于开发机同时构建缓存占了很大的比例。我在这类机器上通常把KEEP_IMAGE_DAYS调成 1构建缓存清理保留小时级让构建机始终保持在“用完即走”的干净状态。在 Jenkins Pipeline 里我习惯把它作为一个独立的定时阶段或者构建后阶段stage(Docker Cleanup) { steps { sh /usr/local/sbin/docker-cleanup.sh || true } }注意我这里加了|| true因为 CI 流水线里清理失败不应该阻塞构建主流程。不过这会掩盖脚本真实的返回码所以日志会起到兜底作用。如果是更复杂的环境比如你已经用 Ansible 管理主机配置那完全可以把脚本放到files/目录然后用 Playbook 分发到所有 Docker 宿主机。脚本本身保持无状态、可重复执行正是适合这种批量管理工具分发的形态。4.4 监控与告警让清理效果可见脚本跑起来之后要能回答一个问题它到底是不是每天在正常工作我在脚本日志里记录清理前和清理后的目录占用然后配置一个简单的磁盘监控任务。如果你有 Prometheus node_exporter可以直接监控node_filesystem_avail_bytes没有也没关系一个简单的df -h定时轮询脚本就够用。我还会把日志文件单独做一个校验如果连续 3 天日志里都没出现“清除完成”之类的结束标记就要怀疑 cron 任务或 systemd timer 是不是挂了。说实话这种“清理工具自己挂了”的问题比磁盘满本身更难察觉。所以日志和监控不是锦上添花而是这套自动化方案的一部分。5. 实战踩坑与排查经验5.1 删了镜像磁盘空间却没减少这是最让人疑惑的现象。我遇到过在开发机上执行完docker image prune -af后df -h一看可用空间几乎没变化。原因通常是以下三个第一删除的镜像层和别的镜像共享了底层并没有真正释放文件。Docker 镜像层是堆叠的一个镜像引用的底层可能被其它镜像继续引用docker image prune只删除不再被任何镜像或容器引用的层。如果都是共享层清理出来的空间自然很小。第二容器还在运行它的可写层和日志文件占着空间。你只清镜像当然看不到效果。先看docker ps把不需要的容器停掉删掉再清镜像。第三日志文件没有被清理。/var/lib/docker/containers/容器ID/容器ID-json.log这个文件会随容器日志无限增长容器删了才好释放。我已经见过无数次“镜像全删、空间没动”的误判最后发现罪魁祸首就是几个几十 GB 的 json 日志。针对日志文件有两个解决思路。临时做法是直接截断truncate -s 0 /var/lib/docker/containers/*/*-json.log更优雅的做法是让容器启动时就限制日志文件大小和数量在/etc/docker/daemon.json中配置{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }改完重启 Docker 或重建容器后每个容器的日志大小就锁在 150MB 以内了。这比任何清理脚本都治本。5.2 提示“image is being used by …”删不掉这个报错是 Docker 的安全保护机制镜像正在被某个容器引用不允许删除。它可能出现在两个场景容器是 running 状态但你清理脚本只处理 exited 容器所以理论上不会碰到。容器被判为 exited但docker rm还没有执行镜像被这个退出容器引用docker image prune会跳过它。我的解决顺序是先删容器再删镜像。这也是脚本里clean_containers在clean_images之前执行的原因。如果你手动操作别忘了即使容器已经退出只要还没rm它依然占着镜像引用。有些镜像被清理失败后挂在那里靠一条docker ps -a就能看到一堆状态Exited (0)的容器都是典型的“该收没收”的状态。5.3 卷清理误伤数据这个坑我给三个字别乱开。docker volume prune -f会把所有不被容器引用的卷全删掉不管卷里是不是存了重要备份。有一次我手滑在一个测试环境开了卷清理结果把同事放在匿名卷里的数据库 dump 给删了。虽然那只是个测试库但对“清理脚本误删数据”这个心理阴影我至今记忆犹新。所以我的最终建议是production 和 pre-production 默认不开启CLEAN_VOLUMES开发机如果要开也要先把KEEP_VOLUME_DAYS调大一点并且每周看日志。数据安全永远比磁盘空间金贵。5.4 cron 环境下 PATH 没有 docker这个是最常见的“脚本偶尔没跑”的元凶。你在终端里能执行 docker不代表 cron 环境能找到它。我在脚本里做了强校验但如果你是从网上抄的别人的脚本很可能不会有这一步而是直接调用docker于是 cron 日志里全是command not found。排查方法很简单crontab -l然后在脚本首行附近明确设置export PATH$PATH:/usr/local/bin:/usr/bin:/bin或者更彻底在 crontab 顶部把 PATH 全局设置好。如果是 systemd timer则需要在 service 文件里加上[Service] EnvironmentPATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin5.5 清理脚本与 CI 构建并发冲突如果清理脚本在你 Jenkins 构建跑到一半时触发有可能出现“正在被使用的镜像”报错严重时导致构建失败。解决思路有两个时间错开jenkins 定时构建放在白天清理脚本放在凌晨。构建机镜像保留天数稍微放宽我曾经把 CI 机器的KEEP_IMAGE_DAYS设为 0结果每次构建都要重新拉一堆基础镜像网络开销反而更大。调成 1 天之后既不会堆积也不会影响当天多次构建。说到底清理脚本的每一次运行都应该被看成一次“低优先级、可被并发安全打断”的后台任务。锁机制虽然防止两个清理脚本并发执行但防不住清理脚本和业务构建之间的竞争。所以调度时间和保留策略就是最后的防护墙。6. 一点点使用体会脚本上线两个月后我最直观的感受是不用再去猜磁盘什么时候满了。docker system df的输出从以前的“几十 GB 可回收”变成“稳定在 10% 以下可回收”告警也从每月数次变成零。不过这并不说明清理脚本万能它解决的只是“没人清理”这个日常问题真正长期健康的 Docker 宿主机还需要你在拉起每个容器的时候就考虑日志大小、数据卷归属和镜像标签规范。清理脚本只是最后一道兜底而不是前期的唯一防线。如果你决定在自己的机器上试跑请一定先从 dry-run 模式和悬空镜像清理开始逐步放开策略让这套自动化的东西先自动“证明自己是安全的”再让它真正自动地跑在你重要的环境里。
返回列表