正直に告白すると、土曜の朝9時から夜9時まで、Windowsサービスという言葉と戦って半日溶かしました。
きっかけは Webhook リスナーでした。外部SaaSから5分おきに通知が飛んでくるのを Python の uvicorn で受けていたんですが、PCを1回再起動した瞬間、当然のように無音で死にました。気づいたのは翌朝、「データが昨日の17時で止まってる」と心臓が冷えた瞬間です。
タスクスケジューラの「ログオン時起動」で粘って2時間、pywin32の自作サービスでレジストリ手探りに90分、Dockerデーモンを検討して「ノートPC1台では重い」と諦めるまで30分。残ったのが NSSM(Non-Sucking Service Manager)という、開き直った名前のOSSでした。これを試した瞬間、3年間の偏見が180秒で崩れました。
やりたかったのは本当に単純な話
要件は5行で済みます。PCが起動したら勝手にPythonが立ち上がる、落ちたら30秒以内に再起動する、ログがファイルに残る、ログオン状態に依存しない、アンインストールも1行で済む。これを全部満たす方法をWindowsで探したら、選択肢はタスクスケジューラ・pywin32・Docker・NSSMの4つでした。最初の2つで合計4時間溶かし、Dockerは外し、NSSMで180秒で片付きました。
NSSMを入れる
公式 [nssm.cc](https://nssm.cc/) からzip 1個を落として、nssm.exe を C:\tools\nssm\ に置くだけで終わります。インストーラーすらありません。PATHを通したら準備完了で、いきなり登録に走った私はここから3つの罠を順番に踏みました。
罠1: PythonがModuleNotFoundErrorで30分黙る
登録は nssm install webhook_listener "C:\Python312\python.exe" "C:\work\webhook\main.py" の1行で、GUIが立ち上がって Install service を押せばサービス一覧に生えます。Start を押すと、すぐに「停止しました」に戻りました。
I/O タブから stdout.log と stderr.log に振り分けて起動し直すと、こう書かれていました。
ModuleNotFoundError: No module named 'fastapi'
webhook を uv venv で C:\work\webhook\.venv\ に隔離していたのに、NSSMは素のシステム Python を呼んでいたわけです。手動運用では activate してから叩いていたので気づきませんでした。解決は2手です。
nssm set webhook_listener Application C:\work\webhook\.venv\Scripts\python.exe
nssm set webhook_listener AppDirectory C:\work\webhook
venv 内の python.exe に切り替えて、作業ディレクトリを揃える。手動運用とサービス化の最大の溝はここでした。タスクスケジューラの時と同じ罠を、また踏み抜いて30分溶けました。
罠2: .envが読めずKeyErrorで90分溶ける
次に出たのが KeyError: 'SLACK_WEBHOOK_URL' です。.env を python-dotenv で読んでいるはずなのに、空でした。原因はサービスの実行ユーザーで、NSSM既定の LocalSystem はホームディレクトリを持たない特殊アカウントです。.env を %USERPROFILE%\.env に置いていたので、~ を解決できず読めなかったわけです。
最初の1時間は ObjectName を自分のアカウントに切り替えるアプローチで粘りました。動きはしますが、パスワード変更のたびに nssm set をやり直す羽目になり、地味に面倒でした。
最終的に選んだのは明示注入です。
nssm set webhook_listener AppEnvironmentExtra `
"SLACK_WEBHOOK_URL=<your-webhook-url>" `
"PYTHONIOENCODING=utf-8"
サービス専用の環境変数を NSSM 側に持たせます。.env の場所もユーザーも関係なく、起動時に必ず注入されます。実体は Windows レジストリのキーなので、漏れにくく、ファイルを誤って Git に上げる事故とも切り離せます。サービス化の文脈ではファイル設定共有は基本的に向かないと、90分溶かしてようやく腹落ちしました。
罠3: 再起動で生き返るかが本番
手動 Start は通った。本当に確かめたかったのは「PCを再起動しても勝手に戻ってくるか」です。設定は3行。
nssm set webhook_listener Start SERVICE_AUTO_START
nssm set webhook_listener AppExit Default Restart
nssm set webhook_listener AppRestartDelay 30000
SERVICE_AUTO_START はWindows起動時に自動で立ち上げる指定で、タスクスケジューラの「PC起動時」と違ってログオン前から走ります。AppExit Default Restart は何で落ちても再起動する宣言。AppRestartDelay 30000 の30秒は、即時再起動で外部APIのレート制限に連鎖死する事故を防ぐクッションです。
ここから私は本当にPCを5回再起動しました。通常・強制電源OFF・Windows Update後・バッテリー切れ想定・ブレーカー落ち想定の5パターンで、5回中5回、webhook_listener は30秒から1分半以内に戻ってきました。3年間「Windowsはサーバー向きじゃない」と思っていた私が、折れた瞬間です。
故意クラッシュも試しました。main.py に 1/0 を仕込み curl で1秒間隔で叩き続けると、30秒ごとに新しい python.exe のPIDが立ち上がって 200 OK を返してくれます。律儀です。
結局たどり着いた15行
3週間運用してみて、登録手順をPowerShell 15行にまとめました。新しい常駐スクリプトを足す時はこれを叩くだけです。
param([string]$N, [string]$Venv, [string]$Script)
$n = "C:\tools\nssm\nssm.exe"
$wd = Split-Path -Parent $Script
& $n install $N "$Venv\Scripts\python.exe" $Script
& $n set $N AppDirectory $wd
& $n set $N AppStdout "$wd\logs\$N.stdout.log"
& $n set $N AppStderr "$wd\logs\$N.stderr.log"
& $n set $N AppRotateFiles 1
& $n set $N AppRotateBytes 10485760
& $n set $N AppEnvironmentExtra "PYTHONUNBUFFERED=1"
& $n set $N Start SERVICE_AUTO_START
& $n set $N AppExit Default Restart
& $n set $N AppRestartDelay 30000
& $n start $N
AppRotateFiles 1 と AppRotateBytes 10485760 で10MB超えたら自動ローテします。入れずに半月運用したら stdout.log が800MBに膨れてエディタが死にました。PYTHONUNBUFFERED=1 を忘れると stdout.log が数分遅れになり、本番クラッシュ時刻が読めなくなります。
個人的に効いた3つの確認手順
1つ目は、必ず手動で python.exe main.py を venv から叩いて、エラーゼロを確かめてから登録すること。素のスクリプトが落ちている状態で登録しても、戻ってくるのは永久ループだけです。2つ目は、Get-Content -Wait -Tail 20 stderr.log を開きっぱなしにして nssm start を叩くこと。ライブで何が起きているかが見え、GUI起動より圧倒的に速いです。3つ目は、必ずPCを1回再起動して、起動から60秒以内に Get-Service で Running を確認すること。これを省くと「実は起動順序の依存があった」事故に本番で当たります。
アンインストールは1行
これがNSSMの最大の安心材料で、nssm remove webhook_listener confirm の1行で、レジストリもサービス一覧も起動エントリも全部きれいに消えます。pywin32の自作サービスをきれいに消すのに小一時間かかった記憶を考えると、もう戻れません。「失敗したら全部捨てられる」という安心感の有無で、試行回数が3倍くらい変わります。
実際にやってみたあとの体感
サービス化して3週間で、私の業務自動化は4本がサービス化済みです。Webhookリスナー、ファイル監視ベースの集計、ローカル MCP サーバー、社内向け簡易キャッシュ。PCを月1で再起動しても、Windows Updateで強制再起動が走っても、たまにブレーカーが落ちても、全部30秒以内に勝手に戻ります。
実際にやってみた感想として、「PCを再起動すると業務が止まる」という隠れたストレスが日常に常駐していたことに、消えて初めて気づきました。私の環境では、サービス化前は週1で「あ、落ちてる」と心臓を冷やしていたのが、ゼロになりました。業務自動化基盤の本質は、スクリプトの精度ではなく「動いていない時間を限りなく短くする仕組み」なんだと思います。
まとめ
罠は3つに集約されました。venv内の python.exe を Application に指定すること、環境変数は AppEnvironmentExtra で明示注入すること、SERVICE_AUTO_START と AppExit Default Restart をセットで入れること。タスクスケジューラより数倍信頼でき、Dockerより1段軽く、pywin32より100倍楽です。半日溶かしましたが、最終的に15行のPowerShellで1本登録できる基盤になりました。
地方の自宅でひとり、ノートPC1台で業務を回す身としては、「PCを再起動できる自由」を取り戻した気分です。週末に静かにサービスを整える時間は、地味ですが、私の業務自動化人生の転換点でした。

コメント