1. はじめに — なぜ Windows サービス化が必要なのか

製造現場の Windows PC で Python アプリを動かす場合、しばしば次のような要件に直面します。

  • PC を再起動しても、誰のログインも待たずに自動でアプリを起動したい
  • ユーザーが画面をロックしたり、ログオフしても処理を継続させたい
  • クラッシュしても自動で再起動させたい
  • 誰かが終了させてしまうリスクを下げたい

これらをすべて満たすのが Windows サービスです(Microsoft 公式ドキュメント)。「サービス」とは Windows 標準の常駐プログラムの仕組みのことで、services.msc(サービス一覧画面)にずらっと並んでいるアレです(Linux を知っている方には systemd ユニット相当、と言えば通じます)。Python アプリをサービス化する方法は大きく 2 系統あり、本記事ではそれぞれの長所短所と実装方法を整理します。

2. 2 つのアプローチ

2.1 pywin32 でネイティブにサービスを書く

  • Windows のサービス API を Python から直接利用
  • サービス停止シグナルを受け取って優雅に終了する処理が書ける
  • 学習コストはやや高い

2.2 NSSM で既存スクリプトをサービス化する

  • NSSM が「サービスのフリ」をして、内部で Python スクリプトを起動・監視・再起動
  • 既存の .py をほぼ無改造でサービス化できる
  • シンプル、運用しやすい、コードを書かなくて済む

結論先取り: 学習コストと運用効率を考えると、多くのケースで NSSM が現実解です。pywin32 ネイティブ実装は、停止シグナルの精密な制御が必要なケースで選びます。

3. NSSM 方式

先に大前提を 2 つ。第一に、サービスの登録・開始・停止・削除はすべて管理者権限が必要です。本章のコマンドは「管理者として実行」で開いた PowerShell(またはコマンドプロンプト)で実行してください。自分の PC に管理者権限がない場合は、本章のコマンド一式を情シスに依頼する形になります。第二に、NSSM は単一の exe でインストーラ不要なので、インターネットに出られない閉域ネットワークの PC にも USB で持ち込んで使えます

3.1 NSSM のインストール

NSSM は単一実行ファイルです。公式サイトから zip をダウンロードし、中の win64\nssm.exe(64bit Windows の場合)を適当なフォルダ(例: C:\tools\nssm)にコピーするだけです(Chocolatey や Scoop——Windows のパッケージ管理ツール——を使っていればそこからも入りますが、使っていなければ手動ダウンロードで十分です)。

以降のコマンドは、フルパス C:\tools\nssm\nssm.exe install ... で実行するか、cd C:\tools\nssm してから .\nssm install ... と実行します(PowerShell はカレントフォルダの exe を nssm だけでは実行できず、.\ の前置きが必要です)。本文では読みやすさのため nssm ... と表記します。

3.2 サービスの登録

対話 UI で登録するか、コマンド一発で登録できます。

nssm install GenbaMonitor

UI が開くので、以下のように設定します。

  • Path: C:\Python312\python.exe(インタプリタへの絶対パス。自分の環境のパスは py -c "import sys; print(sys.executable)" で確認できます。venv を使うなら C:\apps\GenbaMonitor\.venv\Scripts\python.exe のように venv 内の python.exe を指定します)
  • Startup directory: C:\apps\GenbaMonitor(スクリプトのあるフォルダ)
  • Arguments: main.py

または非対話で登録するなら次のようにします。

nssm install GenbaMonitor "C:\Python312\python.exe" "C:\apps\GenbaMonitor\main.py"
nssm set GenbaMonitor AppDirectory "C:\apps\GenbaMonitor"
nssm set GenbaMonitor AppEnvironmentExtra "PYTHONUTF8=1"
nssm set GenbaMonitor Start SERVICE_AUTO_START

3.3 ログのリダイレクト

まずログの出力先フォルダを作っておきます(mkdir C:\apps\GenbaMonitor\logs)。NSSM はログファイル自体は作ってくれますがフォルダは自動作成せず、無いとサービスの起動自体が失敗します。そのうえで、リダイレクトとローテーション(10MB ごと)を設定します。

nssm set GenbaMonitor AppStdout "C:\apps\GenbaMonitor\logs\stdout.log"
nssm set GenbaMonitor AppStderr "C:\apps\GenbaMonitor\logs\stderr.log"
nssm set GenbaMonitor AppRotateFiles 1
nssm set GenbaMonitor AppRotateOnline 1
nssm set GenbaMonitor AppRotateBytes 10485760

標準出力 / 標準エラーが自動でファイルに書き出されます。AppRotateOnline 1 が地味に重要で、これが無いとローテーションはサービスの(再)起動時にしか行われず、再起動しない常駐サービスではログが増え続けます(逆にローテーション不要なサービスでは設定しないのが公式の推奨です——有効時は NSSM がアプリの出力を中継する方式に変わるため)。もう 1 つ注意——NSSM がローテーションしてくれるのは、ここでリダイレクトした stdout / stderr のファイルだけです。Python の logging で自前に開いたログファイルには効きません(そちらは ロギング設計の記事のとおり RotatingFileHandler 側で面倒を見ます)。

3.4 自動再起動の設定

異常終了時に 5 秒待って再起動する設定です(AppThrottle は連続クラッシュ時の最低間隔)。

nssm set GenbaMonitor AppExit Default Restart
nssm set GenbaMonitor AppRestartDelay 5000
nssm set GenbaMonitor AppThrottle 10000

この自動再起動が効くのは、クラッシュだけではありません。停電復旧や計画停電明けに「PLC より PC が先に立ち上がって接続失敗 → 数秒後の自動再起動で救われる」という現場あるあるにも、この設定がそのまま効きます。

3.5 起動・停止・状態確認

nssm start GenbaMonitor
nssm stop GenbaMonitor
nssm status GenbaMonitor

Windows 標準の sc コマンドでも操作できます。sc.exe と拡張子まで書くのがコツ——PowerShell では scSet-Content の別名として解釈され、サービス操作にならずに start という名前のファイルが黙って作られるだけ、という罠があります。

sc.exe start GenbaMonitor
sc.exe query GenbaMonitor

3.6 アンインストール

nssm remove GenbaMonitor confirm

4. pywin32 方式

4.1 ライブラリのインストール

pywin32 は 公式リポジトリ(GitHub) で開発されています。

python -m pip install pywin32
# 初回のみ、サービス登録に必要なポストインストールを実行(管理者権限のプロンプトで)
python -m pywin32_postinstall -install

-m 形式は pywin32 build 309(2025 年 3 月)以降の公式推奨です(それ以前のビルドでは python <venv>\Scripts\pywin32_postinstall.py -install を使います。執筆時点の最新は build 312)。もう 1 つ注意——管理者権限が無いからと pip install --user でユーザー領域に入れると、LocalSystem で動くサービスが pywin32 の DLL を見つけられずエラー 1053 で起動しません。サービス用途の pywin32 はマシン全体にインストールしてください。

4.2 サービススクリプトの基本

# service.py
import logging
import socket
import sys
import threading
import time
from pathlib import Path

import servicemanager
import win32event
import win32service
import win32serviceutil


LOG_PATH = Path(r"C:\apps\GenbaMonitor\logs\service.log")
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
    filename=str(LOG_PATH),
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    encoding="utf-8",
)
logger = logging.getLogger(__name__)


class GenbaMonitorService(win32serviceutil.ServiceFramework):
    _svc_name_ = "GenbaMonitor"
    _svc_display_name_ = "GenbaPy 監視サービス"
    _svc_description_ = "産業機器を監視し、異常検知時にアラートを発するサービス"

    def __init__(self, args):
        super().__init__(args)
        self.stop_event = win32event.CreateEvent(None, 0, 0, None)
        self._stop_flag = threading.Event()
        socket.setdefaulttimeout(60)

    def SvcStop(self):
        logger.info("SvcStop called")
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        self._stop_flag.set()
        win32event.SetEvent(self.stop_event)

    def SvcDoRun(self):
        servicemanager.LogMsg(
            servicemanager.EVENTLOG_INFORMATION_TYPE,
            servicemanager.PYS_SERVICE_STARTED,
            (self._svc_name_, ""),
        )
        try:
            self.main_loop()
        except Exception:
            logger.exception("unhandled exception")
            raise

    def main_loop(self):
        logger.info("service main loop started")
        while not self._stop_flag.is_set():
            try:
                # ここで実際の業務処理(機器との通信、データ収集、判定など)
                self.do_work()
            except Exception:
                logger.exception("error in do_work")
            # stop_event を最大 1 秒待つ。停止要求が来ればすぐ抜ける
            if win32event.WaitForSingleObject(self.stop_event, 1000) == win32event.WAIT_OBJECT_0:
                break
        logger.info("service main loop stopped")

    def do_work(self):
        logger.info("tick")
        time.sleep(0)  # CPU を解放


if __name__ == "__main__":
    if len(sys.argv) == 1:
        # サービスとして起動された場合
        servicemanager.Initialize()
        servicemanager.PrepareToHostSingle(GenbaMonitorService)
        servicemanager.StartServiceCtrlDispatcher()
    else:
        # コマンドライン操作
        win32serviceutil.HandleCommandLine(GenbaMonitorService)

4.3 サービスの登録 / 起動 / 停止

管理者として実行した PowerShell で、用途に応じて 1 行ずつ実行します(上から順に流す一括スクリプトではありません)。--startup auto のようなオプションは、pywin32 の仕様で install より前に書きます(後ろに書くとエラーも出ずに無視されます)。

python service.py --startup auto install
python service.py debug
python service.py start
python service.py stop
python service.py remove

--startup auto で PC 起動時の自動起動になります(install の既定は「手動」)。debug はフォアグラウンドで動かしてログを目視するモードで、install 後にしか使えません(8.1 参照)。

停止時には SvcStop が呼ばれ、メインループに stop_event が伝わります。「優雅に止める」を自前で書けるのが pywin32 方式の利点です。なおサンプルは簡略化のため basicConfig(ローテーションなし)+ 毎秒の tick ログにしています。実運用ではログが増え続けるので、ロギング設計の記事TimedRotatingFileHandler 構成に差し替え、tick のような高頻度ログは出さない(またはサマリ化する)でください。

4.4 PyInstaller との併用

本番では Python ランタイムを各現場 PC に入れずに済ませたいので、PyInstaller で exe 化したものをサービス登録します。pywin32 を含む exe をサービス化するには注意点があり、NSSM で exe を起動するパターンのほうが確実です。詳細は PyInstaller の記事 を参照してください。

5. NSSM と pywin32 の使い分け

  • NSSM が向くケース: 既存スクリプトを最小改造でサービス化したい、停止時にプロセスを kill して終わってよい、ログのリダイレクトとローテーションを楽に済ませたい
  • pywin32 が向くケース: 停止信号を受けて、書き込み中のファイルを安全にクローズ、機器との通信を正常に終了させる必要がある、Windows のイベントログに状態を出したい

多くの製造現場アプリでは、NSSM でラップしつつ、Python 側で停止イベントを受けたときに優雅終了する対応で十分です。ここで重要な Windows の事情——Linux 流の signal.SIGTERM ハンドラは Windows では呼ばれません(外部から SIGTERM が配送される仕組みがないため)。NSSM は停止時にまず Ctrl-C 相当のコンソールイベントを送るので、Python 側には KeyboardInterrupt(SIGINT)として届きます。つまり優雅終了は次の形で書きます。

try:
    main_loop()
except KeyboardInterrupt:
    pass  # NSSM の停止要求(Ctrl-C 相当)はここに来る
finally:
    close_files_and_connections()  # クローズ処理は finally に集約

各段階の猶予は既定 1500ms で、超えると NSSM は次の手段(②WM_CLOSE → ③WM_QUIT——いずれも「終了してください」と求める Windows 内部のメッセージ——→ ④強制終了)に進みます。クローズ処理に時間がかかる場合は nssm set GenbaMonitor AppStopMethodConsole 5000 のようにミリ秒単位で延長できます。注意点として、AppNoConsole 1 を設定すると Ctrl-C が届かなくなり、pythonw.exe(コンソールなし)でもこの方法は効きません。

6. ユーザー権限とセッション

6.1 デフォルトはローカルシステムアカウント

Windows サービスは既定で「ローカル システム」アカウントで動きます。このアカウントは管理者権限を持ちますが、ネットワークドライブにアクセスできないなどの制約もあります。共有フォルダにアクセスする必要があるなら、サービスを「特定のドメインユーザー」アカウントで動かす設定に変更します。

nssm set GenbaMonitor ObjectName "DOMAIN\service-user" "password"

ローカルアカウントなら ".\service-user" の形式です(pywin32 の場合は services.msc の GUI から設定変更)。なお、この方法はパスワードがコマンド履歴に平文で残るため、実行後に履歴を消すか、最初から services.msc の GUI(「ログオン」タブ)で設定するほうが安全です。

6.2 GUI を出さない設計が必須

Windows サービスは原則 GUI セッションから切り離されています。tkinter / PySide6 の窓を出すと、ユーザーから一切見えない不可視のプロセスになるため、サービスは「データ収集・通信・記録」専門に絞り、UI は別アプリ(または Web)で提供する分離設計にしてください。

7. ログの設計

サービスは画面に出力する手段がないため、ログがすべてです。

  • ファイルログ: logging.handlers.RotatingFileHandler または TimedRotatingFileHandler
  • Windows イベントログ: pywin32 の servicemanager.LogMsg で「サービス開始」「サービス停止」「重大エラー」を残す
  • 標準出力 / 標準エラー: NSSM 経由なら自動でファイルに記録
  • 文字コード: encoding="utf-8" 明示は必須(CP932 のままだと業務的に困る)

8. デバッグの定石

8.1 まずフォアグラウンドで動かす

「サービスとして起動した瞬間に止まる」現象の原因の 9 割は、依存ファイルのパスやアクセス権の問題です。特に多いのが、サービス起動時の作業フォルダは C:\Windows\System32 になるため、開発中は動いていた open("config.ini") のような相対パスが突然読めなくなるパターンです(9 章で「絶対パス」を求めるのはこのためです)。python service.py debug や、コマンドプロンプトで普通に起動して、フォアグラウンドで動くことを確認します。なお debug はレジストリのサービス情報を参照するため、先に install を済ませていないと「The service does not appear to be installed.」で終了します。手順は ①install → ②debug で目視確認(Ctrl+C で停止)→ ③start の順です。

8.2 「Python が見つからない」

サービスにはあなたのユーザー環境変数の PATH が適用されません(適用されるのはシステム環境変数の PATH のみ。Python を「自分だけにインストール」した場合はここに入っていません)。python.exe絶対パスで指定するのが必須です。

8.3 「ファイルの場所がおかしい・書き込めない」

ローカル システムアカウントで動くサービスの %USERPROFILE%Path.home())は、あなたのユーザーフォルダではなく C:\Windows\System32\config\systemprofile を指します。開発時と違う場所に読み書きして「ファイルが見つからない / どこかに書いてるが見えない」となるのが典型です。ユーザーフォルダに依存せず、アプリ用フォルダ(C:\apps\ など)にログ・設定を置き、実行アカウントにそのフォルダの書き込み権限を付与します。

8.4 タスクスケジューラとの違い

「タスクスケジューラ」も常駐用途で使えますが、サービスのほうが優れている点が多くあります。

  • 稼働状態の確認(sc.exe query)・停止 / 開始が標準操作として揃っている(タスクスケジューラでも「スタートアップ時」トリガー + SYSTEM アカウントでログオン不要の起動自体は可能だが、状態管理の作法が揃わない)
  • クラッシュ時の自動再起動(回復オプション)が標準機能
  • services.msc で一覧は誰でも確認でき、操作(開始・停止)は管理者のみ——つまり夜勤者の誤操作で止められにくく、運用の引き継ぎもしやすい

「定時に 1 回動くスクリプト」はタスクスケジューラで十分ですが、「常駐し続ける」用途は迷わずサービスにします。タスクスケジューラ側で「登録した Python が動かない」場合の切り分け(0x1 / 0x2 / 0x80070002 / 0x41301 系の終了コードの読み方、作業ディレクトリが System32 になる問題、venv・文字コードの罠)は、タスクスケジューラで Python が実行されない — 戻り値 0x1 / 0x2 から原因を切り分けるで詳しく扱っています。

9. 24 時間運用のチェックリスト

  • 絶対パス: スクリプト・ログ・設定ファイルすべて絶対パスで指定
  • UTF-8 モード: 環境変数 PYTHONUTF8=1 をサービス登録時に設定(※ PyInstaller 6+ で exe 化したアプリには効かないため、その場合はビルド時の --python-option "X utf8" とコード側の encoding="utf-8" 明示で対応。詳細は 文字コード記事
  • 停止処理: ファイル書き込み・通信切断・キュー flush を finally で確実に実行
  • 自動再起動: 失敗時に再起動、ただし無限ループは避ける(最低間隔を設ける)
  • ログ: ファイルログ + イベントログの 2 系統
  • 監視: 「サービスが動いているか」を別系統で監視(外部監視サーバー、社内 Slack 通知など)
  • バージョン情報: 起動時にバージョンをログに必ず出す。トラブル時に「どの版で起きたか」が即わかる

10. おわりに

Python アプリを Windows サービス化すると、現場 PC の運用は一気に安定します。「再起動を恐れない」「ユーザーログオフを気にしない」「クラッシュしても勝手に立ち直る」——これらの性質は、24 時間動くデータ収集や監視アプリを現場に展開するときの大きな安心材料になります。

NSSM で素早く立ち上げ、停止処理が複雑になってきたら pywin32 ネイティブ実装に乗り換える——という段階的アプローチが、多くのプロジェクトで現実的です。

⚠️ 実機運用時の注意: 本記事のサンプルは検証用(ローカル環境での動作確認想定)です。実際の本番機(現場の 24 時間稼働 PC・監視サーバ)でサービスとして登録・常駐させる場合は、変更管理プロセス・保全側の承認を経てから実施してください。サービスのインストール・削除・アカウント変更・自動起動設定の操作は管理者権限を要求し、失敗すると PC 全体の起動やユーザーログオンに影響することがあります。ドメインユーザーアカウントでの実行やパスワード投入は、情シス・AD 管理者と事前調整のうえで行ってください。本記事のコードによる損害・事故(サービス障害・データ喪失・監視停止を含む)について筆者・GenbaPy は責任を負いません。

関連記事

参考文献・一次情報