引っ越しをした翌週から、サーバーが毎晩勝手にシャットダウンするようになりました。

クラッシュでも再起動でもなく、電源が落ちる形の死に方です。起動すれば必ず ext4 recovery が走り、uptime はゼロに戻り、直前まで動いていたものは何の遺言も残していません。しかも時間帯を選びます。ほとんどが明け方でした。

最初に疑ったのはケーブルの緩みで、全部挿し直しました。翌日また死にました。引っ越し後にエアコンと給湯器が同じ回路に乗っている可能性を考えて切ってみました。また死にました。温度を測ると 50 から 60°C、まったく正常。最後は引っ越しの振動で PSU が壊れたのではと疑い始めます。買って一年も経っていない Super Flower の Platinum で、あまり信じたくない話でした。

問題は、これが全部推測だということです。しかも一つ検証するのに丸一日かかります。何日か追ったところで、足りないのは仮説ではなく証拠だと気づきました。死んだその瞬間、マシンに入っていた電気がどうなっていたのかを、私は何も知らなかったのです。

そこで UPS を買いました。半分は保護のため、もう半分は計測器として使うためです。

この記事はその後の全過程です。UPS の選び方、NUT をゼロから自動安全シャットダウンまで設定する手順、絶対に間違えられない BIOS の項目、そして最後に UPS のログが事件を解決するまで。犯人は猫にかじられた電源ケーブルでした。

なぜ UPS で決着がつくのか#

なぜそれが計測器になるのかを先にはっきりさせます。以下すべての論理がそこに乗っているからです。

UPS が測っているのは自分自身の出力側の状態で、今なにをしているかを短いコードで報告します。この記事に何度も出てくるので先に並べておきます。

ステータス意味
OLOn Line。商用電源は正常で、UPS は横で待機しているだけ
OBOn Battery。商用電源が落ちてバッテリー放電中
LBLow Battery。残量わずか、シャットダウンすべき状態

この三つがあれば、記録を取り続けている UPS は次の死を二つにきれいに切り分けられます。

  • 死んだ瞬間に OB または電圧降下を記録していた → 問題は UPS の上流、商用電源側です。
  • 死んだ瞬間まで OL のまま、電圧も安定、バッテリーも満充電 → UPS が送り出した電気はきれいなので、問題は下流。つまり電源ケーブル、PSU、マザーボードの区間です。

この境界線だけで探索範囲が半分になり、しかも推測に頼らずに済みます。

選定:1500VA / 正弦波 / AVR 付き#

CyberPower CP1500PFCLCD には、今回の診断にとって意味のある点が三つありました。

PFC 正弦波出力

今どきの PC 用電源はほぼアクティブ PFC です。この種の電源は安価な UPS が出すと相性が悪く、バッテリーへ切り替わる瞬間にそのまま落ちることがあります。それは今まさに追っている症状とまったく同じ新しい問題を自作するということで、診断中に一番いらないものです。

AVR 内蔵

は仮説を一つ丸ごと消してくれます。明け方に本当に電圧変動が起きているなら、その場で平らにならしてくれる。ならした上でまだ死ぬなら、商用電源という線は消せます。

NUT が完全対応

Linux 側は usbhid-ups ドライバで読めるので、メーカー製の専用ソフトは要りません。USB を挿すと lsusb にこう出ます。

console
Bus 001 Device 005: ID 0764:0601 Cyber Power System, Inc.

NOTE

LAN ケーブルは挿さなくていい この UPS は USB とシリアルの両方を持っていますが、NUT は USB だけで足ります。ネットワーク側は管理カード付きの機種や複数台で共有する構成のためのもので、単体監視では出番がありません。

NUT:ゼロから自動安全シャットダウンまで#

NUT の設定ファイルはいくつかの場所に散らばっていて、しかも権限が合っていないとサービスがそもそも起動しません。全部を一本のスクリプトにまとめてあるので、再インストールやマシン交換のときはそれを流すだけにしています。

まず NUT の三層を理解する#

設定ファイルがごちゃごちゃに見えるのは、NUT が一つのプログラムではなく、役割の違う三つのプログラムだからです。どれが何かを掴めば、設定ファイルは自然に整理されます。

  • driver(うちは usbhid-ups)—— 実際に UPS と会話する部分で、メーカーのプロトコルを NUT のデータモデルに翻訳します。
  • upsd —— driver が取ったデータをサービスとして公開し、他から問い合わせられるようにします。
  • upsmon —— クライアント側。ずっと upsd にデータを要求し続け、残量が低くなったらシャットダウンを実行します。

そう考えると、五つの設定ファイルはこう割り振られます。

bash
/etc/nut/nut.conf      # このマシンの役割(単体か、クライアント/サーバーか)
/etc/nut/ups.conf      # driver: どのドライバで、どの UPS に繋ぐか
/etc/nut/upsd.conf     # upsd: データサービスがどのアドレスで待ち受けるか
/etc/nut/upsd.users    # upsd: 誰が読めて、誰がコマンドを出せるか
/etc/nut/upsmon.conf   # upsmon: どういう条件で落とし、どう通知するか

単体監視なら standalone を使います。

ini
# /etc/nut/nut.conf
MODE=standalone
ini
# /etc/nut/ups.conf
maxretry = 3
pollinterval = 5

[cyberpower]
    driver = usbhid-ups
    port = auto
    desc = "CyberPower CP1500PFCLCD"
ini
# /etc/nut/upsd.conf —— localhost のみ、外には出さない
LISTEN 127.0.0.1 3493

WARNING

LISTEN のデフォルトは変えること NUT の upsd0.0.0.0 で待ち受けていると、同じ LAN の誰でも UPS の状態を照会でき、権限設定が甘ければコマンドまで出せてしまいます。単体監視で外に出す理由はまったくないので 127.0.0.1 に縛ります。パスワードは openssl rand -hex 16 で生成してそのままファイルに書いています。人間が覚える必要はないので。

権限も合わせないと、driver が自分の設定を読めません。

bash
chown root:nut /etc/nut/*.conf /etc/nut/upsd.users
chmod 640 /etc/nut/*.conf /etc/nut/upsd.users

安全なシャットダウンはどう起きるのか#

肝は upsmon.conf のこの数行です。

ini
MONITOR cyberpower@localhost 1 upsmon <password> primary
MINSUPPLIES 1
SHUTDOWNCMD "/sbin/shutdown -h +0"
POWERDOWNFLAG /etc/killpower
POLLFREQ 5
POLLFREQALERT 5
FINALDELAY 5

一行ずつ見ていきます。

ディレクティブ何をしているか
MONITORどの UPS をどの資格情報で監視するか。末尾の primary は、このマシンがシャットダウン時に UPS へ電源遮断を指示する権限を持つという意味
MINSUPPLIES動作継続に最低いくつの電源が生きている必要があるか。単一電源のマシンなら 1
SHUTDOWNCMDシャットダウン時に何を実行するか
POWERDOWNFLAGシャットダウン処理が残すフラグファイル。後で詳しく
POLLFREQ / POLLFREQALERT平常時と異常時、それぞれ何秒ごとに upsd へ問い合わせるか
FINALDELAYシャットダウンコマンドを出す前の最後の猶予秒数

実際の流れはこうで、最後の段が一番よく抜けるところです。

POWERDOWNFLAG が指しているのはフラグファイルです。upsmon はシャットダウンを決めた時点でまずそれを作り、システムのシャットダウン処理は最後にそれを見にいきます。ファイルがあれば、ついでに UPS へ自分の出力も切るよう指示するわけです。

これをやらないと、マシンは落ちたのに UPS は放電を続け、バッテリーがゼロまで搾り取られます。 そして鉛蓄電池は深放電のたびに寿命を削られます。

そして BIOS が自動起動 の段を設定していないと、その前の全部が無駄になります。マシンはきれいにシャットダウンして、そのまま誰かが電源ボタンを押しに来るのを待ち続けることになるからです。

シャットダウン閾値の決め方#

トリガーになるのは UPS 自身が低残量と判断する点です。うちの機種の工場出荷値はこれでした。

console
battery.charge.low: 10        # 残量 10%
battery.runtime.low: 300      # または推定残り 300 秒

まず出荷値のまま使うのは意図的です。誤作動が一番起きにくい値だからです。しばらく走らせて実際の数字が出てから、早めに倒すかどうかを決めます。今の実測はこうです。

12%
UPS 負荷
定格 1000W
61
推定稼働時間
113V
入力電圧

負荷 12%、稼働時間一時間なら、残り 5 分からシャットダウンを始める余裕で十分すぎます。ただ UPS を 60% まで使っていて稼働時間が十数分しかないなら 5 分は少し際どいので、upssched でもっと早いトリガー(たとえば「バッテリー運転が 90 秒続いたら落とす」)を足すことになります。

BIOS:間違えられない一項目#

自動復旧の流れ全体で、一番簡単に台無しにできる場所です。

CAUTION

Restore after AC Power LossOnLast State ではない Last State は「電源が落ちる前の状態に戻る」という意味です。ところが NUT の低残量シャットダウンの後、その状態は電源オフです。だから商用電源が戻ったとき BIOS は「もともと切れていたのだから」と判断してオフのままにします。自動復旧を一式組み上げて、最後の一段で止まるわけです。 On のほうが「電気が来たら起動する」という意味で、直前の状態は問いません。

うちのボードは MSI MAG X870E TOMAHAWK なので、経路はこうです。

text
起動時に Del → F7 で Advanced Mode
  → SETTINGS → Advanced → Power Management Setup
    → Restore after AC Power Loss = On

BIOS は OS より前なので、SSH もリモートデスクトップも届きません。物理的なモニタとキーボードが要ります。Del を連打する手間を省きたいなら、先にこれを打っておけます。

bash
sudo systemctl reboot --firmware-setup

そのまま BIOS 設定画面へ再起動します。ただし人間は画面の前にいる必要があります。

UPS の状態を外に出す#

設定が済めば upsc でいつでも見られますが、毎回 SSH で入ってコマンドを打つのは面倒でした。

毎分ログを一行残す#

このスクリプトを cron で毎分回します。人間が読むログ一行と、プログラムが読む JSON を同時に書きます。

bash
DATA=$(upsc cyberpower 2>/dev/null) || exit 0
g() { grep -m1 "^$1:" <<<"$DATA" | cut -d' ' -f2-; }

echo "$(date '+%F %T') status=$(g ups.status) in=$(g input.voltage)V \
out=$(g output.voltage)V load=$(g ups.load)% batt=$(g battery.charge)% \
runtime=$(g battery.runtime)s" >> "$OUT"

そのログ行はこうなります。そしてこれが後に事件を解決したものです。

console
2026-07-05 00:12:01 status=OL in=113.0V out=113.0V load=12% batt=100% runtime=3675s
2026-07-05 00:13:01 status=OL in=113.0V out=113.0V load=12% batt=100% runtime=3675s
2026-07-05 00:14:01 status=OL in=113.0V out=113.0V load=12% batt=100% runtime=3675s
                    ↑ この直後にマシンが強制断

イベントを Discord へ#

upsmon.conf はイベント種別ごとに外部コマンドを紐づけられます。NOTIFYCMD を一行足し、通知したいイベントのフラグに +EXEC を付けるだけです。

ini
NOTIFYCMD /path/to/ups-discord-notify.sh
NOTIFYFLAG ONBATT   SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT  SYSLOG+WALL+EXEC
NOTIFYFLAG ONLINE   SYSLOG+WALL+EXEC

通知しているイベントは六種類です。バッテリーへの切り替え、残量低下でまもなくシャットダウン、シャットダウン実行、商用電源の復帰、バッテリー劣化で交換時期、UPS との通信断。それぞれに現在の残量、残り分数、負荷、商用電源の電圧を添えています。

Webhook のファイルは upsmon が読める場所、所有者 root:nut、パーミッション 640 に置く必要があります。最初にやったとき詰まったのがここで、ホームディレクトリの下に置いていたので upsmon から読めていませんでした。

サブドメインをもう一つ増やしたくない#

Home Assistant の NUT 統合や、peaNUT のようなパネルを立てることも考えました。ただ自分のサブドメインはすでにかなりの数があって、UPS 一台を見るためだけにサイトをもう一つ立てるのは過剰です。

なので自前の NAS ダッシュボードに組み込むことにしました。ここで一つ制約があります。バックエンドは docker の bridge ネットワークで動いていて、ホスト側の upsd:3493 に届きません。upsd を外に晒すくらいなら、ホストがファイルを書いてコンテナが読む形にします。

JSON は先ほどのスクリプトがついでに吐いているものです。

json
{
  "status": "OL",
  "online": true,
  "battery_charge": 100,
  "battery_runtime": 3675,
  "ups_load": 12,
  "input_voltage": 113.0,
  "model": "CP1500PFCLCDa TW",
  "updated_at": "2026-08-04T13:55:01+0800"
}

このやり方はバックエンドが元から他のマウント済みファイルを読んでいる構成と揃いますし、読み取り専用の値ひとつのためにネットワーク権限を開けずに済みます。

NOTE

ついでに見つかったこと 途中でスマホから監視画面が見られないことに気づき、PWA の問題だと思い込みました。違いました。そもそも PWA が存在しません。 manifest も service worker もありません。スマホで表示されるのは完全に別のモバイル版レイアウトで、監視ページはデスクトップ版の Dock にしか置いていなかったのです。つまり「PWA に載せ忘れた」のではなく「モバイル版に最初から無かった」。この「A の問題だと思ったら B だった」というパターンは、この一件の中で一度きりではありませんでした。

解決:猫にかじられたケーブル#

UPS を導入して数日後、また一度死にました。

ログを見返すと、死ぬ前後は全期間 OL、商用電源は 113 から 115V で安定、バッテリー 100%、OB イベントは一件もなしnut-monitor にも何も残っていません。それでも起動時はまた ext4 recovery でした。

入ってくる電気はきれいなのに、マシンは強制的に落ちている。問題は UPS の下流で確定です。

範囲が電源ケーブル・PSU・マザーボードまで絞れたので、ケースを開けて全コネクタを挿し直し、ついでに電源ケーブルも交換しました。元のものは 12A 125V 定格で、そこに猫がかじった穴が開いていました。

そのケーブルの位置はちょうど UPS の出力から PSU の入口までで、UPS の視界からは完全に外れています。銅線が半分切れたケーブルは間欠的に接触します。少しの振動や熱膨張・収縮で、通じては切れ、また通じる。それがまさに「ランダムで、瞬間的で、前兆のない」電源断の正体でした。

しかもこれで、それまで辻褄の合わなかったところが全部説明できます。

現象かじられたケーブルでの説明
UPS のログが全期間きれい断点が UPS の測定点より下流なので、そもそも見えない
アイドルでも死ぬ負荷とは無関係で、機械的な接触不良だから
引っ越し後から始まった運搬の振動で、半分切れた銅線が離れやすくなった
明け方を選ぶ気温が最も低く、熱収縮で接点が縮む

新しく付けたケーブルは 10A 125V です。定格が下がったように見えますが、10A × 125V は 1250W。マシンのフル負荷はおよそ 350 から 400W で、換算すると 3A に届きません。余裕は非常に大きい計算です。

1
交換前
死ぬ平均間隔
10.8
交換後
連続稼働

「ほぼ毎晩死ぬ」から「十日に一度死ぬ」へ。頻度が十分の一になりました。

危うく誤誘導された再発#

ケーブル交換から十一日目、また死にました。しかもうちのゲーム機も再起動していました

二台同時に落ちたなら、結論は明白に見えます。本当に停電したのだと。そこから推論が一気に伸びます。本当に停電したなら UPS がサーバーを支えるはずで、支えなかったということは電源ケーブルが「Battery Backup」ではなく「Surge Only」の列に挿さっていたに違いない。CyberPower の背面はその二列がほとんど同じ見た目で、挿し間違いは非常によくある話です。

その一連の推論はもっともらしく組み上がっていました。ゲーム機がなぜ再起動したのかを調べるまでは。

Windows Update でした。

前提が崩れ、推論も丸ごと無効になります。ログを見直すと、その死に方は前の四回とまったく同じでした。OL のまま、商用電源は安定、バッテリー満充電、OB なし。停電でもなければ挿し間違いでもなく、同じ「UPS 下流の瞬断」が、十分の一の頻度で起きただけです。

TIP

独立した二つの事象が同時に起きるのは、推論を踏み外す最短経路 「二台同時に落ちた」は、これ以上ないほど強い共通原因の証拠に見えます。ただの偶然でした。新しい証拠が出てきて仮説が急に非常に説得力を持ったとき、それこそがその証拠自体を検証し直すべき瞬間です。Windows Update の履歴を見にいっていなかったら、一晩まるごと UPS のコンセント分類を調べていたはずです。

ではその回は結局なんだったのか。正直に言うと、分かっていません。

その時点で確定できたのは消去法の結果だけです。商用電源ではない(UPS のログは全期間きれい)し、過熱でもない(死んだ瞬間 CPU は 85% idle、load 1 から 3、温度アラートはゼロ、温度も正常)。残る可能性で一番高いのは、新しいケーブルの片端が奥まで入っていなかったか、内部のコネクタがまた少し緩んだか。C13/C14 の「挿さった感じはするが実は奥まで行っていない」は非常によくあります。両端を挿し直して、そこで話は途切れました。

途切れたのは諦めたからではなく、四日後にまったく別の死に方でもう一度死んだからです。そこから注意が完全にそちらへ移り、この残留した不具合は検証できるほどの頻度で再発しなくなりました。だから正直な結論はこうなります。かじられたケーブルが頻度を十分の一にして、残りの一回は今も未解決。

ただその日、もう一つだけやったことがあります。「過熱」がやや早めに除外されたのが引っかかったので、温度ロガーをついでに仕込みました。CPU 温度と load を毎分記録するだけの、次回のための保険です。

四日後、それが即座に働きました。

console
15:12  cpu=90.1C  load=8.65
15:22  cpu=90.0C  load=32.53
15:31  cpu=90.0C  load=84.42
15:43  cpu=90.0C  load=156.54
15:50  cpu=90.0C  load=196.01   ← cron がもうスケジュールされない

その回は電源断ではなく、システムが焼き切れていました。温度は 90°C に張り付いたまま四十分、load は 8 から 196 まで上がり、その間 UPS はずっと正常。電気とはまったく無関係の、別の話です。

NOTE

追跡のたびに計測器が一つ残り、次の事件は前の事件が残した計器で解かれる UPS は電源断を調べるために買ったもので、そのログがかじられたケーブルを暴きました。温度ロガーは過熱を除外するために足したもので、四日後に熱暴走の現行犯を押さえました。これは運が良かったのではありません。「確信が持てない」たびに足したのが推測ではなく記録装置だった、というだけのことです。そういう記録装置は、自分が必要になると気づく前から証拠を溜め始めてくれます。

書いている時点での状況#

journalctl --list-boots を見れば、各再起動が「きれいなシャットダウン」だったのか「強制断」だったのかが分かります。末尾に systemd-shutdown があるのが前者、何も残っていないのが後者です。

console
07-14 → 07-15 20:21   シャットダウン記録なし   ← 残留した電源不具合
07-16 → 07-19 15:56   シャットダウン記録なし   ← 熱暴走
07-19 → 07-20 17:15   systemd-shutdown
07-20 → 以降八回       systemd-shutdown        ← すべて自分で再起動したもの

最後の計画外の死は 07-19 で、その後十六日間は強制断が一度もありません。

ただしこの残留した電源不具合を解決済みとは呼べません。 この期間に色々な理由で八回再起動しているので、単一の連続稼働は最長で七日しかなく、一方で前回の電源不具合が出るまでの間隔は 10.8 日でした。つまり今のところ、前回の発生間隔を超えた連続稼働は一度もありません。直ったと証明されたのではなく、まだ出る機会がなかっただけです。

本当に解決宣言をするなら、まず十一日を超えて静かに走り続ける必要があります。この基準は自分で決めたもので、あえて書いているのは、「それ以来起きていません」という一文は時間の尺度を添えなければ証拠として成立しないからです。

一ページのチェックリスト#

UPS + NUT セルフホスト チェックリスト
  1. UPS 選定は三点:PFC 正弦波(アクティブ PFC の電源は波形を選ぶ)、AVR 付き(電圧変動でバッテリーを使わない)、NUT 対応(メーカー製ソフトが不要)。
  2. UPS は計測器でもある:ups.status / input.voltage / battery.charge を毎分記録しておけば、次の障害を「商用電源の問題」と「UPS 下流の問題」にきれいに切り分けられる。
  3. upsd127.0.0.1 に縛る。単体監視で外に出す理由はない。
  4. 設定ファイルの権限は root:nut 640。合っていないと driver が起動しない。
  5. POWERDOWNFLAG を抜かさない。抜くと OS 停止後にバッテリーがゼロまで搾られる。
  6. BIOS は OnLast State ではない。間違えると NUT のきれいなシャットダウン後、商用電源が戻ってもマシンは起きてこない。
  7. シャットダウン閾値はまず出荷値。実際の稼働時間の数字が出てから upssched を検討する。
  8. Webhook ファイルは /etc/nut の下にupsmon はホームディレクトリを読めない。
  9. コンテナへ状態を渡すならファイル経由。ホストが JSON を書いて bind-mount するほうが、upsd を外に晒すよりきれい。
  10. ケーブルは配線モールか、かじられ防止のスリーブへ。これはサーバーとは無関係で、猫のためです。
  11. 仮説を一つ潰すたびに記録装置を一つ残す。次に何か起きたときには、もう手元にあります。
參考連結