午前3時、監視アラートが当番用スマホを震わせた。/ パーティション使用率 98%、MySQL が binlog を書けず、CI の docker pull が no space left on device で止まった。SSH で入り df -h を実行すると、ルートには 412MB しか残っていなかった。マルウェアではなく、3か月放置されたログ、Docker レイヤー、apt キャッシュ、journalが 40GB の小さな VPS ディスクをじわじわ食いつぶしていた。
中小規模 VPS ではこの障害パターンが非常に多い。ディスクは小さいのに、コンテナは 24 時間稼働し、イメージを pull し、ログは止まらない。手動の rm -rf は遅く、誤削除のリスクも高い。実用的な手順はまず容量の大きい場所を特定し、再生成可能なディレクトリをスクリプトで一括クリーンアップすることだ。本記事では本番環境で何度も使ってきたワンクリック清理スクリプトをそのまま使える形に整理し、安全に自動化できる操作と、再確認が必要な操作を区別して示す。
前提:SSH ログイン権限と sudo があること。接続自体できない場合(SSH 鍵が書き込めないなど)は、まず Linux サーバー SSH 接続拒否のトラブルシューティングで、ディスク満杯なのかネットワーク/ファイアウォールの問題なのかを切り分ける。空き容量を確保する前に UFW ルールをいじる必要はない。まずスペースを空け、サービスがログを書ける状態に戻すことが先だ。
ディスクが満杯になると、最初に何が起きる?
ルートファイルシステムが満杯になると、Linux の症状は「謎の不具合」のように見える。SSH は入れるのに vim が保存できない、apt install が失敗する、Docker コンテナが起動しない、Nginx が 500 を返すのに error.log が増えない。MySQL / PostgreSQL は読み取り専用になったりクラッシュしたり、systemd サービスは pid ファイルや socket を書けずに再起動を繰り返す。Cron のログに追記できずジョブが静かに失敗することもある。
最初の一手は常にマウントポイントの確認であり、「大きそうなフォルダ」を当てることではない。別ボリュームの /data に空きがあってもルートが詰まっているケースは珍しくない。df -hT なしで「ディスク満杯」を一つの数字として扱うと、間違った調査に時間を溶かす:
# 各パーティションの空き容量 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 について説明されている。bind mount や別ボリュームがあるとき、クロスマウントの合計値に惑わされないこと。
よくある「ディスク食い」ディレクトリ一覧
スタックは環境ごとに異なるが、Ubuntu / Debian VPS では次のパスが頻繁に上位に来る。Web + Docker + CI を 40GB ディスク一台に載せる構成は定番で、journal とコンテナレイヤーは静かに増え、アクセスログはトラフィック急増時に一気に膨らむ。表の自動清理の可否欄に「要確認」とある行は、依存関係を確認してから触ること。迷ったら du で dry-run し、lsof +D /path でプロセスのファイル保持状況を見てから削除する。
| パス / ソース | 典型的なサイズ | 自動清理 | 備考 |
|---|---|---|---|
/var/lib/docker |
数 GB〜数十 GB | 一部可 | ダングリングイメージ、停止コンテナ、未使用 volume。稼働中コンテナのデータ volume は削除しない |
/var/log/journal |
1〜10+ GB | 可 | systemd journal は上限未設定だと急速に膨らむ |
/var/log/*.log |
数百 MB〜GB | 要確認 | truncate または logrotate。直接 rm すると fd が残り 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、オブジェクトストレージのマウント、バックアップディレクトリが最大なら、キャッシュ清理では救えない。アーカイブ、ディスク拡張、コールドデータ移行が必要だ。スクリプトが回収できるのは再生成可能な領域だけであり、容量計画の代わりにはならない。
ワンクリック清理スクリプト(コピペ可)
以下のスクリプトはデフォルトでドライラン。解放される見込みの容量と実行予定コマンドを表示し、--apply を付けたときだけ実際に削除する。必ず先に dry-run し、出力を確認してから apply すること。dry-run を飛ばして古いベースイメージだけが残っていたサービスを prune し、デプロイパイプラインを一から組み直したチームも見てきた。モジュール分割されているので、Docker 未導入なら Docker ブロックを、proxy_cache 未使用なら Nginx ブロックをコメントアウトできる。
#!/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 は未使用イメージをすべて削除する。次回デプロイでフル re-pull が必要になり、帯域の狭い VPS では復旧が遅くなる。--filter until=168h で 1 週間未使用のレイヤーだけに絞るか、定期的に 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 を保持したままで、ファイル削除だけでは inode がすぐ解放されない。長期的には logrotate で Nginx やアプリログを日次ローテーションし、N 世代保持する。
apt:apt-get clean は /var/cache/apt/archives のみ。autoremove --purge は旧 kernel と孤立依存を削除する。kernel アップグレード後は現行と一つ前を残し、新 kernel が起動失敗したときにロールバックできるようにする。本番では uname -r と dpkg -l 'linux-image-*' を照合してから autoremove するのが無難だ。
業務データには触れない:/var/lib/mysql、/var/lib/postgresql、Redis RDB、ユーザーアップロード、バックアップマウントはスクリプトの対象外。du でそれらが最大なら、アーカイブ、ディスク拡張、コールドストレージ移行が必要で、キャッシュ清理では足りない。
清理後:ディスクが再び静かに満杯になるのを防ぐ
ルート使用率を 98% から 60% に下げただけでは始まりに過ぎない。ガードレールがなければ journal が再び 8GB に膨らみ、デプロイのたびに Docker レイヤーが積み上がり、トラフィック急増で同じインシデントが再発する。中小 VPS 向けの最低限の予防は次の3点:
- アラート閾値:クラウドコンソールまたは Prometheus で 80% 警告、85% で SMS/ページャー。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 を設定する。1 コンテナの stdout だけでディスクを埋めるケースは、孤立イメージと同じくらい多く、見落とされがちだ。
対応前チェックリスト
次回アラートが来たら、順番にチェックするために runbook に写しておく:
df -hTとdf -ih— 容量不足か inode 枯渇かdu -shで docker、journal、log、cache を点検- スクリプト dry-run → 目視確認 →
--apply - 清理後
df -hで検証し、DB・Nginx・Docker を spot-check - journald 制限、logrotate、cron、80% アラートを追加
ディスク満杯は「ファイルをいくつか消す」で終わる事象ではなく、容量とログポリシーの健康診断だ。スクリプトは午前3時の慌てた rm から、再現可能な標準手順へ移す。何を消して df がどれだけ動いたかを記録すれば、欠けていた logrotate やローテーション未設定のコンテナログが見えてくる。残りは予防で、ページャを静かに保つことだ。
ビルド成果物がディスクを食う?役割分担を検討
VPS で本番サービスと xcodebuild、多段 Docker ビルド、Ollama モデルキャッシュを同時に回すと、小さなディスクはコンパイル週に DerivedData とイメージレイヤーで一気に埋まる。本番ログと同じ SSD を奪い合う構図になる。
より健全な分離は本番 VPS を lean に保ち、重いビルドを別環境へ——クラウド Mac で iOS パッケージ、専用 Runner で CI、本番は最終成果物だけ pull する。
Mac mini M4 の統合メモリと低アイドル消費電力は、「ビルド専用・24時間トラフィックを載せない」ノードに向いている。Linux VPS と役割分担すれば、コンパイルキャッシュが本番ログと同じディスクを争わない。本番機のスリム化とビルド環境分離を計画しているなら、まず重い作業をクラウド Mac へ移す——VPSSpark プランを見る。午前3時の df -h との格闘を減らそう。