VPSが9時間固まった夜 — コンテナ1つの再起動から ext4 読み込み専用まで
VPSが9時間固まった夜 — コンテナ1つの再起動から ext4 読み込み専用まで
「unhealthy だから再起動しとこう」。その一手で、VPS 全体が9時間応答しなくなり、最後はファイルシステムが読み込み専用になった記録。おまけに、復活したエージェントが勝手に API 課金を再開していた。
環境
- Xserver VPS(メモリ 6GB / Swap 2GB)
- 1台に Docker コンテナ 17個(Dify 一式、n8n、OpenClaw、Git サーバーなど)
- コンテナにメモリ上限なし
何が起きたか(時系列)
| 時刻 | 出来事 |
|---|---|
| 数週間前〜 | OpenClaw が Up (unhealthy) のまま放置。OOM killer も何度か発動していた |
| 15:01 | docker restart で OpenClaw を再起動 |
| 15:01〜 | ログが途絶。CPU スパイク、ディスク読込が 100MB/s で張り付き |
| 〜23:58 | Web もSSHもシリアルコンソールも無反応 |
| 23:58 | 管理パネルから再起動 → OS がシャットダウンに失敗 → 強制断 |
| 翌 00:00 | 起動3秒後に ext4 エラー → ルートが読み込み専用に → Docker 起動不可 |
| 翌 09:00 | シャットダウン → イメージ保存 → 起動。起動時の自動 fsck で修復 |
症状から原因を読む
1. 「ポートは開くのに応答しない」=スラッシング
nc -z <host> 22 # succeeded
ssh <host> # Connection timed out during banner exchange
TCP の受付はカーネルがやるので通る。でも sshd や nginx が返事をする余力がない。パネルのグラフではディスク読込が一定値に張り付いていた。メモリが足りず、ページをディスクから読み直し続けている状態。
Swap があると OOM killer が動かず、ログに Out of memory が残らないことがある。 「OOM ログがない=メモリの問題ではない」ではない。
2. 送信量のグラフで「侵入」を否定する
ログに海外 IP からの root 総当たりが大量に出ていて一瞬焦ったが、すべて [preauth] で切断。パネルの転送量(送信)も障害中ほぼゼロで、外部への持ち出しは見られなかった。
3. 読み込み専用の原因は「再起動の仕方」
EXT4-fs error (device vda3): ext4_mb_generate_buddy: group 479,
block bitmap and bg descriptor inconsistent
Aborting journal on device vda3-8.
EXT4-fs (vda3): Remounting filesystem read-only
前回起動のログは23:58まで書けていた=固まっている間ディスクは正常。壊れたのは、固まった OS が正常に poweroff できず強制断になったとき。errors=remount-ro なので、不整合を見つけた ext4 が自分を読み込み専用にした。
役に立ったコマンド
# 前回起動のログ(persistent journal が有効なら読める)
journalctl --list-boots
journalctl -b -1 -n 15 --no-pager
journalctl -b -1 --since "YYYY-MM-DD 14:55" --until "YYYY-MM-DD 15:15" --no-pager
# 今回起動の最初の ext4 エラー(dmesg はリングバッファが上書きされがち)
journalctl -k -b 0 --no-pager | grep -iE "ext4|i/o error|read-only" | head
# 次回起動で自動 fsck が走るか
tune2fs -l /dev/vda3 | grep -i state
# clean with errors → 走る見込み / clean → 走らない
# コンテナ ID から名前を引く(dockerd が止まっていても読める)
grep -o '"Name":"[^"]*"' /var/lib/docker/containers/<id前方>*/config.v2.json
復旧手順
- 読み込み専用のうちは何も書かない(データを読み出すには安全な状態)
- 管理パネルで「シャットダウン」→「イメージ保存」(バックアップ)
- 「起動」→ 起動時の自動 fsck で修復されることを期待
- 直らず
(initramfs)等に落ちたら、コンソールでfsck -y /dev/vda3 tune2fs -lでcleanを確認
Xserver VPS のパネルには「強制停止」やメモリのグラフはない。電源操作は起動・再起動・シャットダウンのみ。
おまけ1:白画面の正体は末尾スラッシュ
復旧後、https://example.com/openclaw がまっしろ。DevTools を見ると /assets/index-xxx.js が 404。
HTML が ./assets/... の相対パスで読んでいるので、
/openclawで開く →/assets/...を探す → 404/openclaw/で開く →/openclaw/assets/...→ 正常
URL の末尾に / を付けるだけで直った。
おまけ2:復活したら API 課金も復活していた
ダッシュボードに入ると、ハートビート(定期的にエージェントを起こす仕組み)が Claude Sonnet で動いていた。コンテナが死んでいた2か月は止まっていたのに、復旧した瞬間に再開。
ざっくり試算(Sonnet 4 = 入力$3 / 出力$15 per 1M tokens)
| 設定 | 1回 | 30分間隔 | 6時間間隔 |
|---|---|---|---|
| 既定(会話履歴を毎回送る ≈100K tokens) | ≈$0.3 | ≈$15/日 | ≈$1.2/日 |
isolatedSession + lightContext(≈2〜5K tokens) |
≈$0.01 | ≈$15/月 | ≈$1/月 |
| 上記 + Haiku | ≈$0.003 | ≈$5/月 | ほぼ0 |
API 従量課金で既定設定のまま放置すると、何もしていないのに枯渇する。
止める・安く再開する
# 停止(話しかけた時の返答や Cron Jobs は動く)
openclaw config set agents.defaults.heartbeat.every "0m"
# 再開するなら、長い間隔 + 履歴を送らない + 安いモデル
openclaw config set agents.defaults.heartbeat.every "6h"
openclaw config set agents.defaults.heartbeat.isolatedSession true
openclaw config set agents.defaults.heartbeat.lightContext true
openclaw config set agents.defaults.heartbeat.model "<provider>/<cheap-model>"
Docker なら先頭に docker exec <container> を付ける。キー名は公式ドキュメント(Heartbeat)準拠で、バージョンによって違う可能性あり。
ローカル LLM(Ollama)にすれば API 代はゼロだが、同じ 6GB の VPS に載せると今回と同じことが起きかねない。やるなら別マシンで。
再発防止
# 暴れても OpenClaw だけが落ちるように
docker stats --no-stream docker-openclaw # 先に現在値を確認(通常 約470MB だった)
docker update --memory=1.5g --memory-swap=1.5g docker-openclaw
通常時ぎりぎり(1g)ではなく余裕を持たせたのは、事故の引き金が起動時の一時的なメモリ消費だったから。ぎりぎりだと起動のたびに OOM kill → 再起動ループになりうる。
docker update はコンテナ再作成で消えるので、compose にも書く:
openclaw:
mem_limit: 1.5g
memswap_limit: 1.5g
その他:
unhealthyを放置しない。検知したら通知する- 再起動の前に
docker logsとfree -hを見る - 大きな操作の前にイメージ保存
- コンテナを復活させたら、定期実行(ハートビート等)の課金も確認
- AI に手順を聞いたら、パネル操作やサービス固有の仕様は公式マニュアルで裏を取る(今回も存在しない「強制停止」ボタンを提案された)
教訓
「とりあえず再起動」は、余裕のあるシステムでだけ効く。 メモリに余裕のない1台に全部載せている環境では、その一手が全体を巻き込む。