はじめに
Dockerを長く運用していると、ある日突然 no space left on device でコンテナが起動しなくなる、ということがよく起きます。
主な原因は次のとおりです。
- 使われなくなった古いイメージ
- ビルドキャッシュ(BuildKit)
- 停止したままのコンテナ
- ローテーションされていないコンテナログ(json-file)
- 使われていないボリューム
この記事では、Dockerホストのディスク容量を保つために必要な設定と、定期メンテナンスの内容、自動化の方法をまとめます。
動作確認環境の想定: Linux(systemd採用のディストリビューション)、Docker Engine 23.0以降、ストレージドライバ overlay2
1. 現状を把握する
まずは何が容量を使っているかを確認します。
ホスト全体
df -h
df -h /var/lib/docker
df -i /var/lib/docker # inode枯渇もあわせて確認
overlay2は小さなファイルを大量に作るため、容量に余裕があってもinodeが先に枯渇するケースがあります。df -i も必ず見ておきます。
Dockerの内訳
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 42 8 18.5GB 14.2GB (76%)
Containers 12 8 1.1GB 230MB (20%)
Local Volumes 15 6 9.8GB 3.4GB (34%)
Build Cache 230 0 6.7GB 6.7GB
RECLAIMABLE が削除によって回収できる容量の目安です。詳細は -v で確認できます。
docker system df -v
コンテナログのサイズ
docker system df にはコンテナログのサイズが含まれないので、別途確認します。
sudo sh -c 'du -h /var/lib/docker/containers/*/*-json.log' | sort -h | tail -n 10
ファイル名のIDとコンテナ名の対応は次で確認できます。
docker ps -a --no-trunc --format '{{.ID}} {{.Names}}'
2. 先にやっておくべき恒久対策
定期削除の前に、「そもそも溜まりにくくする」設定を入れておきます。
2-1. コンテナログのローテーション
デフォルトのログドライバ json-file は サイズ上限なし です。これがディスク逼迫の原因の筆頭になります。
/etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
この例ではコンテナ1つあたり最大30MB程度に抑えられます。
ログ収集基盤(Fluentdなど)でjsonファイルを直接読んでいない場合は、圧縮とローテーションが組み込まれている local ドライバも選択肢です。
daemon.jsonのログ設定は 新しく作成したコンテナにのみ 適用されます。
既存コンテナに反映するには、systemctl restart docker の後にコンテナの再作成(docker compose up -d --force-recreate など)が必要です。
Docker Composeでサービスごとに指定することもできます。
services:
app:
image: example/app:latest
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
2-2. ビルドキャッシュの自動GC
ビルドを行うホストでは、BuildKitのガベージコレクションを有効にしておきます。
{
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "20GB"
}
}
}
builder.gc 配下で指定できるキーはDockerのバージョンによって差があります。利用中のバージョンの公式ドキュメントを確認してください。
2-3. journaldの上限設定
Docker自体ではありませんが、ホストのログも地味に容量を食います。
/etc/systemd/journald.conf
[Journal]
SystemMaxUse=1G
sudo systemctl restart systemd-journald
2-4. data-rootを専用ディスクに分ける(設計段階で可能なら)
/var/lib/docker がルートファイルシステムと同居していると、Dockerの肥大化がOS全体を止めてしまいます。
新規構築時であれば、専用ディスクをマウントして data-root を指定しておくと被害を局所化できます。
{
"data-root": "/data/docker"
}
3. 定期メンテナンスで実行するコマンド
3-1. 停止中のコンテナ
docker container prune -f --filter "until=168h"
until で「作成から一定時間経過したもの」に限定できます(168h = 7日)。
3-2. イメージ
# タグのない(dangling)イメージのみ
docker image prune -f
# どのコンテナからも参照されていないイメージを全て(7日以上前に作成されたもの)
docker image prune -a -f --filter "until=168h"
-a を付けると未使用イメージがすべて対象になります。停止中コンテナが参照しているイメージは消えません。
ただし、次回デプロイ時に再pullが発生する点には注意してください。
消したくないイメージがある場合は、ラベルで除外できます。
docker image prune -a -f --filter "until=168h" --filter "label!=keep=true"
3-3. ビルドキャッシュ
docker builder prune -f --filter "until=168h"
3-4. ネットワーク
容量への影響は小さいですが、ついでに掃除しておきます。
docker network prune -f --filter "until=168h"
3-5. ボリューム(自動化しない)
docker volume ls -f dangling=true
docker volume prune
ボリュームには データベースなどの永続データ が含まれます。定期ジョブでの自動削除は避け、中身を確認したうえで手動で実施してください。
Docker 23.0以降の docker volume prune はデフォルトで匿名ボリュームのみが対象です。名前付きボリュームも含める -a オプションは特に慎重に扱ってください。
同様の理由で、docker system prune -a --volumes を定期ジョブに入れるのはおすすめしません。
3-6. やってはいけないこと
-
/var/lib/docker/overlay2配下をrmで直接削除する
→ Dockerの管理情報と不整合を起こし、コンテナやイメージが壊れます。必ずdockerコマンド経由で削除します。 - 稼働中コンテナのログファイルを
rmする
→ ファイルハンドルが開いたままのため容量が解放されません。緊急時はtruncateで空にします(ログは失われます)。
sudo truncate -s 0 "$(docker inspect --format='{{.LogPath}}' <コンテナ名>)"
4. 自動化する(systemd timer)
cronでも構いませんが、実行履歴をjournalで追えるsystemd timerを使った例を示します。
4-1. クリーンアップスクリプト
/usr/local/sbin/docker-cleanup.sh
#!/usr/bin/env bash
set -euo pipefail
KEEP="168h" # 保持期間
echo "=== docker cleanup start: $(date '+%F %T') ==="
df -h /var/lib/docker
docker system df
docker container prune -f --filter "until=${KEEP}"
docker image prune -a -f --filter "until=${KEEP}" --filter "label!=keep=true"
docker builder prune -f --filter "until=${KEEP}"
docker network prune -f --filter "until=${KEEP}"
echo "--- after ---"
docker system df
df -h /var/lib/docker
echo "=== docker cleanup end: $(date '+%F %T') ==="
sudo chmod 755 /usr/local/sbin/docker-cleanup.sh
4-2. service / timer
/etc/systemd/system/docker-cleanup.service
[Unit]
Description=Docker periodic cleanup
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/docker-cleanup.sh
Nice=10
IOSchedulingClass=idle
/etc/systemd/system/docker-cleanup.timer
[Unit]
Description=Run docker cleanup weekly
[Timer]
OnCalendar=Sun *-*-* 03:00:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now docker-cleanup.timer
# 確認
systemctl list-timers docker-cleanup.timer
# 手動実行
sudo systemctl start docker-cleanup.service
# 実行ログ
journalctl -u docker-cleanup.service -n 100
Persistent=true にしておくと、ホスト停止中に予定時刻を過ぎた場合でも起動後に実行されます。
5. 監視(しきい値アラート)
定期削除だけでは急増に対応できないため、使用率の監視も入れておきます。
Zabbix・Prometheusなどの監視基盤がある場合はそちらを使い、ない場合は簡易スクリプトでも最低限の検知ができます。
/usr/local/sbin/disk-check.sh
#!/usr/bin/env bash
set -euo pipefail
TARGET="/var/lib/docker"
WARN=80 # %
usage=$(df --output=pcent "$TARGET" | tail -n 1 | tr -dc '0-9')
iusage=$(df --output=ipcent "$TARGET" | tail -n 1 | tr -dc '0-9')
if [ "$usage" -ge "$WARN" ] || [ "$iusage" -ge "$WARN" ]; then
msg="[WARN] $(hostname) ${TARGET} disk=${usage}% inode=${iusage}%"
logger -t disk-check "$msg"
# 通知先に合わせて変更(Slack Webhookの例)
# curl -s -X POST -H 'Content-type: application/json' \
# --data "{\"text\":\"${msg}\"}" "$SLACK_WEBHOOK_URL"
fi
これを1時間ごとなどのtimerで回しておきます。
6. 運用サイクルのまとめ
| 頻度 | 作業 | 方法 |
|---|---|---|
| 常時 / 1時間ごと | ディスク・inode使用率の監視 | 監視ツール or disk-check.sh |
| 毎週 | 停止コンテナ・未使用イメージ・ビルドキャッシュ・ネットワークの削除 | docker-cleanup.timer |
| 毎月 |
docker system df -v とログサイズの確認、未使用ボリュームの棚卸し |
手動 |
| 構築時 / 設定変更時 | ログローテーション、BuildKit GC、journald上限、data-root分離 | daemon.json など |
おわりに
Dockerホストのディスク対策は、次の3段構えにしておくと安定します。
- 溜まりにくくする設定(ログローテーション、BuildKit GC)
- 定期的な削除(prune系コマンドの自動実行)
- 早めの検知(使用率とinodeの監視)
特にコンテナログの上限設定は忘れられがちで、効果も大きいので、まだの方はまずここから入れてみてください。