コンテンツへスキップ

Flaskの開発サーバーを「デスクトップアプリのバックエンド」として使う際の注意点

Flaskでアプリを起動すると、ターミナルにこう表示される。「WARNING: This is a development server. Do not use it in a production deployment.」——開発用サーバーなので本番デプロイに使うな、という警告である。ところがローカルFlaskサーバーをそのままアプリ本体として同梱し、エンドユーザーの手元でずっと動かし続ける構造のデスクトップアプリは珍しくない。この警告を無視しているように見えて、実は前提となる条件が違う。何が変われば安全に読み替えられ、逆に何を自分で手当てしないと事故になるのかを整理する。

補足: WSGI(Web Server Gateway Interface)とは、PythonのWebアプリケーションとWebサーバーの間の標準インターフェース。Flask自身はWSGIアプリケーションを組み立てる役割で、実際にHTTPリクエストを受け付けて捌く「サーバー」の部分は別物として差し替え可能になっている。開発時にデフォルトで動くのが軽量な組み込みサーバー(Werkzeug)で、本番運用では別途 Gunicorn 等の「本番用WSGIサーバー」に差し替えるのが通常の作法である。

警告が想定しているリスクは「インターネット上の不特定多数」

この警告が本来指しているのは、不特定多数のクライアントからの同時アクセスをさばききれない、セキュリティ強化が最小限、複数ワーカープロセスでの負荷分散ができない、といった「公開Webサービスとして使うには力不足」という話である。

デスクトップアプリのバックエンドとしてFlaskを使う場合、この前提が根本的に変わる。サーバーは 127.0.0.1(ループバック)だけにバインドし、外部ネットワークからは一切到達できない。アクセスしてくるのは同じマシン上で動く1つのブラウザタブだけであり、「不特定多数」ではなく「ユーザー自身が開いた1枚のタブ」に閉じている。この条件下では、開発用サーバーの弱点の多くはそもそも問題にならない。

裏返せば、うっかり host='0.0.0.0' を指定してしまうと同じLAN上の他端末からもアクセスできる状態になり、この前提が崩れる。ループバックだけにバインドすることは明示的に守る必要がある。

自動リロードは切る — 二重プロセスの温床になる

Flaskの開発サーバーには、ソースコードの変更を検知して自動的にプロセスを再起動する reloader 機能がある。開発中は便利だが、この仕組みは内部的にファイルの変更監視スレッドを持つ親プロセスと、実際にリクエストを処理する子プロセスの2プロセス構成で動く。

パッケージ化されたデスクトップアプリでこれが有効なままだと問題が起きる。起動時に確保したポート番号や、多重起動防止のために書き出す「実行中サーバーの情報ファイル」は「今動いているプロセスは1つだけ」を前提に設計する。reloaderが親子2プロセスを作ると、この前提が崩れて、ポートの二重確保やプロセス監視の迷子といった不具合につながりかねない。

# 開発時の利便性より、単一プロセスであることの保証を優先する
app.run(port=port, use_reloader=False, threaded=True)

use_reloader=False を明示することで、常に単一プロセスとしてアプリが起動することを保証する。デスクトップアプリではソースコードの実行時変更は起こらないため、reloaderの恩恵自体がそもそも不要という側面もある。

threaded=True は明示する — さもないと同時アクセスで詰まる

Flaskの開発サーバーは、明示的に指定しない限り、リクエストを1件ずつ順番に処理する。デスクトップアプリのUIがブラウザ上で動く以上、実際には複数のリクエストが同時に飛んでくる場面が普通にある。たとえば「保守処理の進捗をストリーミングで受け取り続ける長時間のリクエスト」と「生存確認のために数十秒おきに送る短いハートビートリクエスト」が同時に発生する構成では、前者がサーバーを占有している間、後者が待たされ続けることになる。

# threaded=True を明示しないと、長時間リクエストの裏で
# 短いリクエストが待たされ続けるケースがありうる
app.run(port=port, use_reloader=False, threaded=True)

threaded=True を指定すると、リクエストごとに別スレッドで処理されるようになり、この待ち行列問題を避けられる。

スレッドで同時実行されるということは、共有状態の競合に備える必要があるということ

threaded=True によって複数リクエストが同時に処理されるようになると、今度は別の問題が持ち上がる。複数のリクエストハンドラが同じグローバル変数を同時に読み書きする可能性が生まれる。

たとえば「今どの保守処理が実行中か」を保持するグローバル変数があるとする。あるリクエストが「実行中かどうかを確認 → 実行中でなければ新しく開始する」という2ステップの処理をしている最中に、別スレッドが同じ変数を読み書きすると、両方が「実行中でない」と判定して二重に処理を開始してしまう、いわゆる check-and-set の競合状態が起こりうる。

from threading import Lock

_maint_lock = Lock()
_maint_process = None

def start_maintenance():
    with _maint_lock:
        if _maint_process is not None:
            return False  # 既に実行中
        # ここでロックを保持したまま新規プロセスを起動する
        ...
        return True

対処は単純で、共有変数の読み書きを threading.Lock で保護し、「確認してから書き込む」までを1つの不可分な操作として扱う。これは複数プロセス間の排他制御を扱うファイルロック(fcntl / msvcrt)とは層が異なる話である点に注意したい。ファイルロックは「別プロセスとの競合」を防ぐ仕組みであるのに対し、こちらは同一プロセス内の「別スレッドとの競合」を防ぐ仕組みであり、threaded=True を選んだ時点でこちらへの備えが必須になる。

ポートの確保と多重起動検知は自分で実装する

本番用のアプリケーションサーバーであれば、複数インスタンスの起動制御やポートの調停は、プロセス管理の仕組み(systemdやコンテナオーケストレーションなど)が担うのが普通である。デスクトップアプリのFlaskバックエンドにはそういう外部の管理者がいないため、これらを自前で用意する必要がある。

具体的には、決まったポートを固定で使うと「同じアプリを二度起動しようとしたら、前のプロセスがまだポートを掴んでいて起動できない」という単純な失敗を起こしやすい。空いているポートを順番に探して確保し、選んだポート番号とプロセスIDを一時ファイルに書き出しておくことで、次回起動時に「既に動いているインスタンスがあるか」を判定できるようにする。

import socket

def find_free_port(start=5002, end=5100):
    for port in range(start, end):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.bind(('127.0.0.1', port))
                return port
            except OSError:
                continue
    raise OSError(f"ポート {start}〜{end} の範囲で空きポートが見つかりませんでした")

既存インスタンスを検出した場合はエラーで終了するのではなく、そのポートへブラウザを開き直して自分自身は終了する設計にしておくと、ユーザーから見れば「アイコンをもう一度クリックしたら元の画面に戻ってきた」という自然な挙動になる。

ブラウザタブが閉じたことを、サーバー自身は知らない

ネイティブのウィンドウアプリなら、ユーザーがウィンドウを閉じた瞬間にOSがそのイベントをアプリに通知してくれる。ところがブラウザタブをUIとして使う構造では、タブが閉じられてもバックエンドのFlaskプロセスにはそれを知る手段がない。何も対策をしなければ、ユーザーがタブを閉じてもバックエンドプロセスだけがバックグラウンドに残り続けてしまう。

この隙間を埋めるのが、ブラウザ側から定期的に送る「ハートビート」リクエストである。

_last_heartbeat_time = 0.0

@app.route('/api/heartbeat', methods=['POST'])
def api_heartbeat():
    global _last_heartbeat_time
    _last_heartbeat_time = time.time()
    return ('', 204)

ブラウザ側が数十秒おきにこのエンドポイントを叩き続け、サーバー側は別スレッドで「最後にハートビートを受け取ってから一定時間が経過していないか」を定期的にチェックする。タブが閉じられればハートビートは止まるので、一定時間の沈黙を「ユーザーがタブを閉じた」とみなして自分自身を終了させる。保守処理の実行中は誤って終了しないようにガードを入れ、起動直後のブラウザ接続待ちの時間帯もウォームアップ期間として除外しておくと、実運用での誤作動を避けられる。

まとめ

設定・仕組み 目的
127.0.0.1 のみにバインド 外部ネットワークからの到達を遮断する
use_reloader=False 単一プロセスであることを保証する
threaded=True 長時間リクエストが短いリクエストを詰まらせないようにする
threading.Lock で共有状態を保護 スレッド間の check-and-set 競合を防ぐ
空きポート探索 + 実行中プロセス情報の記録 多重起動を検知・回避する
ハートビート監視 + タイムアウトで自己終了 タブが閉じられたことを間接的に検知する

Flaskの開発サーバーは「使ってはいけないもの」ではなく、「何を前提にした警告なのかを理解した上で、その前提が成り立つ環境に限定して使うもの」だと捉えると扱いやすい。ループバック限定・単一プロセス・スレッド安全性・多重起動検知・生存確認、これらを一つずつ手当てすることで、開発用サーバーをデスクトップアプリの実行基盤として安定して運用できる。

「なぜそもそもFlask + ブラウザという構造を選んだのか」という設計判断自体は、デスクトップアプリを「ローカル Flask + ブラウザ UI」構造で書いた判断で扱っている。今回の記事はその構造を選んだ後に踏むべき、開発サーバー運用の具体的な注意点というくくりになる。