凌晨三点,监控告警把值班手机震醒:/ 分区使用率 98%,MySQL 写不进 binlog,CI 部署在 docker pull 阶段直接报 no space left on device。我们连上机器跑 df -h,根分区只剩 412MB——不是突然被黑客塞了木马,而是三个月没人管的日志、Docker 层、apt 缓存和 journal慢慢把 40GB 小盘吃光了。
这类故障在中小 VPS 上极其常见:套餐磁盘不大,业务却 7×24 跑容器、拉镜像、打日志。手动 rm -rf 既慢又容易误删;更靠谱的做法是先定位大户,再用脚本批量清理「可再生」目录。本文把我们生产环境里反复用过的一键清理脚本整理成可复制版本,并标明哪些操作安全、哪些需要二次确认。
前提:你已有 SSH 登录权限和 sudo。若此刻已经连不上(例如 SSH 密钥写不进去),请先参考 Linux 服务器 SSH 连接被拒排查指南,确认是磁盘满导致还是网络/防火墙问题。磁盘救回来之前,别急着改 UFW 规则——先把空间腾出来,服务才能正常写日志。
磁盘满了,你会先看到什么?
Linux 在根分区写满时,表现往往很「玄学」:SSH 还能登录,但 vim 保存不了、apt install 失败、Docker 容器起不来、Nginx 返回 500 且 error.log 不再增长。MySQL / PostgreSQL 可能直接只读或崩溃;systemd 服务反复 restart,因为无法创建 pid 文件或写 socket。
第一步永远是看挂载点,而不是猜哪个目录「看起来大」:
# 各分区剩余空间 df -hT # 根目录下一层谁最大(可能较慢) sudo du -xh --max-depth=1 / 2>/dev/null | sort -hr | head -20 # inode 用尽也会「假满」 df -ih
du 在文件特别多时会扫很久;若机器已经卡死,可只对可疑路径点查,例如 sudo du -sh /var/lib/docker /var/log /var/cache。官方 du(1) 手册 对 -x(不跨文件系统)和 --apparent-size 有详细说明——跨盘挂载时别被 du 总数误导。
常见「吃盘」目录对照表
不同业务组合不同,但下面几类在 Ubuntu / Debian VPS 上出现频率最高。表里标注了是否适合脚本一键清——带「谨慎」的请先确认没有业务依赖再动。
| 路径 / 来源 | 典型体积 | 可否自动清 | 说明 |
|---|---|---|---|
/var/lib/docker |
数 GB~数十 GB | 部分可 | 悬空镜像、停止的容器、未用 volume;运行中容器的数据卷勿删 |
/var/log/journal |
1~10+ GB | 可 | systemd 日志默认无上限时膨胀极快 |
/var/log/*.log |
数百 MB~GB | 谨慎 | 可 truncate 或 logrotate;直接 rm 可能导致进程仍占 inode |
/var/cache/apt |
数百 MB~数 GB | 可 | apt-get clean 安全,只删已下载 deb 包 |
/tmp、/var/tmp |
不定 | 部分可 | 删 7 天未访问文件;注意是否有程序把临时上传放这里 |
Nginx proxy_cache |
数 GB | 可 | 缓存可重建;若你按我们 Nginx 反向代理调优 配了 cache,清完会短暂 MISS |
旧内核 /boot |
数百 MB | 可 | apt autoremove --purge 删旧 kernel,保留当前与上一个 |
| core dump、崩溃转储 | 不定 | 可 | /var/crash、应用目录下的 core.* |
/home、/var/lib/mysql、对象存储挂载点或备份目录最大,脚本清缓存帮不上忙——需要归档、扩容或迁走冷数据。清理只能回收「可再生」空间,不能代替容量规划。
一键清理脚本(可复制)
下面脚本默认干跑(dry-run):只打印将释放的空间与将执行的命令,加 --apply 才真正删除。建议先 dry-run,确认输出合理后再 apply。脚本按模块拆分,你可以注释掉不需要的部分(例如机器没装 Docker 就跳过 Docker 段)。
#!/usr/bin/env bash # 服务器磁盘一键清理 — 仅处理可再生缓存 # 用法: sudo ./disk-cleanup.sh # 预览 # sudo ./disk-cleanup.sh --apply # 执行 set -euo pipefail APPLY=false [[ "${1:-}" == "--apply" ]] && APPLY=true log() { echo "[$(date '+%F %T')] $*"; } run() { if $APPLY; then log "EXEC: $*" eval "$@" else log "DRY-RUN: $*" fi } before=$(df -h / | awk 'NR==2 {print $3 " used, avail " $4}') log "Before: $before" # 1. apt 缓存 if command -v apt-get >/dev/null; then run "apt-get clean -y" run "apt-get autoremove -y --purge" fi # 2. systemd journal — 保留最近 7 天或 500MB if command -v journalctl >/dev/null; then run "journalctl --vacuum-time=7d" run "journalctl --vacuum-size=500M" fi # 3. Docker 未使用资源(不含命名 volume 内业务数据) if command -v docker >/dev/null && docker info >/dev/null 2>&1; then run "docker system prune -af --filter 'until=168h'" run "docker builder prune -af --filter 'until=168h'" fi # 4. 临时目录 — 7 天未访问 run "find /tmp /var/tmp -type f -atime +7 -print 2>/dev/null | head -20" if $APPLY; then find /tmp /var/tmp -type f -atime +7 -delete 2>/dev/null || true fi # 5. Nginx proxy_cache(按你的 cache_path 修改) NGINX_CACHE="/var/cache/nginx" if [[ -d "$NGINX_CACHE" ]]; then run "du -sh '$NGINX_CACHE'" run "find '$NGINX_CACHE' -type f -delete 2>/dev/null || true" fi # 6. 崩溃转储 [[ -d /var/crash ]] && run "rm -rf /var/crash/*" # 7. pip / npm 缓存(若存在) [[ -d /root/.cache/pip ]] && run "rm -rf /root/.cache/pip/*" for u in /home/*; do [[ -d "$u/.npm" ]] && run "npm cache clean --force --cache '$u/.npm' 2>/dev/null || rm -rf '$u/.npm/_cacache'" done after=$(df -h / | awk 'NR==2 {print $3 " used, avail " $4}') log "After: $after" $APPLY || log "预览模式结束。确认无误后: sudo $0 --apply"
赋予执行权限:sudo chmod 750 /usr/local/sbin/disk-cleanup.sh。第一次务必不加 --apply,把 DRY-RUN 输出存一份;若 Docker 段显示将删除大量镜像,确认没有「只剩本地、仓库已删」的唯一副本。
docker system prune -a 会删掉所有未使用的镜像,下次部署要重新 pull,在带宽紧张的 VPS 上可能反而拖慢恢复。更稳妥的是加 --filter until=168h(一周未用才删),或定期清理 dangling 镜像:docker image prune -f。详见 Docker 官方 pruning 文档。
分模块说明:清什么、不清什么
journalctl:--vacuum-time 与 --vacuum-size 可组合使用,systemd 会按 journalctl 文档 规则裁剪二进制日志。若你希望永久限制体积,在 /etc/systemd/journald.conf 里设 SystemMaxUse=500M 后 systemctl restart systemd-journald。
日志文件:对仍在被进程打开的大日志,用 truncate -s 0 /var/log/nginx/access.log 比 rm 安全——进程仍持有 fd,rm 只删目录项,空间不会立刻释放。长期应配 logrotate,让 Nginx / 应用日志按天切割并保留 N 份。
apt:apt-get clean 只清 /var/cache/apt/archives;autoremove --purge 会删旧内核和孤儿依赖。升级内核后建议保留「当前 + 上一版」两个 kernel,防止新内核启动失败时无法回退。
业务数据别碰:/var/lib/mysql、/var/lib/postgresql、Redis RDB、用户上传目录、备份挂载点——这些不在脚本默认范围内。若 du 显示它们最大,需要的是归档、扩容或迁冷存储,而不是清缓存。
清理之后:别让磁盘再次悄悄满
救急脚本跑完,根分区从 98% 降到 60% 只是开始。我们给中小 VPS 的最低配预防是三件事:
- 监控阈值:云厂商或自建 Prometheus 在 80% 告警,85% 发短信——别等 95% 才有人看。
- 每周 cron:
0 4 * * 0 root /usr/local/sbin/disk-cleanup.sh --apply >> /var/log/disk-cleanup.log 2>&1 - journald + logrotate 上限:把「无限增长」改成「有顶的天花板」。
若跑 Docker Compose 栈,给 json-file 日志加 max-size 和 max-file,否则单个容器的 stdout 就能把盘写满——这和清镜像一样重要,却常被忽略。
上线前检查清单
把下面几条抄进你的运维笔记,下次告警来时按顺序打勾:
df -hT与df -ih— 确认是空间满还是 inode 满du -sh点查 docker、journal、log、cache 目录- 脚本 dry-run → 人工扫一眼 →
--apply - 清理后
df -h验证,并抽查核心服务(DB、Nginx、Docker)是否正常 - 补 journald 限制、logrotate、cron 与 80% 告警
磁盘满从来不是「删几个文件」就结束的运维事件,而是一次容量与日志策略的体检。脚本负责把你在凌晨三点的手速从「慌乱 rm」拉回到「可重复的标准流程」——剩下的,是把预防补上,让告警少响几次。
开发构建占满磁盘?考虑分工部署
若你的 VPS 既要跑生产服务,又要本地 xcodebuild、Docker 多层构建或 Ollama 模型缓存,小盘很容易在编译周被 DerivedData 和镜像层挤爆。更稳妥的做法是生产 VPS 保持精简,把重构建迁到专用环境——例如云端 Mac 跑 iOS 出包、独立 Runner 跑 CI,生产机只拉最终产物。
Mac mini M4 凭借统一内存与低功耗,适合作为「只干构建、不扛 7×24 业务流量」的节点;与 Linux VPS 分工后,生产盘只留运行时数据,编译缓存不再和生产日志抢同一块 SSD。
如果你正在规划「生产机瘦身 + 构建环境分离」,可以先把重活迁到云端 Mac,让 VPS 回归轻量运维——了解 VPSSpark 套餐,少在凌晨三点和 df -h 搏斗。