Uvicornを用いたFlaskの本番起動について

Flaskで作成したAPIを本番環境で起動する方法について説明します。Traefikをリバースプロキシとして利用し、UvicornでFlaskアプリケーションを起動します。

    Loading...

2026-02-15-02-27-53

## 想定する基本的なディレクトリ構成

flaskアプリのディレクトリ構成
.
├─ templates
   ├─ index.html
   └─ result.html
├─ Dockerfile
├─ app.py      # Flaskアプリケーションのエントリーポイント
├─ asgi.py     # ASGIアプリ化を行うためのファイル
├─ compose.yml
├─ pyproject.toml
└─ uv.lock

app.py にはFlaskアプリケーションのコードが記述されています。 これらは uv で管理されているプロジェクトとしています。

app.py
from flask import Flask, render_template

app = Flask(__name__)

def solve_problem():
    results = ...
    return results

@app.route('/', methods=['GET'])
def index():
    return render_template('index.html')

@app.route('/solve', methods=['POST'])
def solve():
    results = solve_problem()
    return render_template('result.html', results=results)

if __name__ == '__main__':
    app.run(debug=True, port=5000, threaded=True)

## アプリ開発時のFlaskの起動方法

uv run コマンドで開発サーバーを起動します。

uv run flask run

このコマンドを実行すると、Flaskに内蔵されている簡易サーバー (Werkzeug) が起動し、http://localhost:5000 でアプリケーションにアクセスできるようになります。

しかしWerkzeugは開発用のサーバーであり、本番環境での使用は推奨されていません。

  • セキュリティ的に脆弱である
  • パフォーマンスが低い
  • 安定性や運用機能がない

ことが理由です。

## 本番環境でのFlask起動方法

本番環境では、GunicornuWSGI といったWSGIサーバー、あるいは Uvicorn といったASGIサーバーを使用してFlaskアプリケーションを起動します。 これらはPython向けの本番用サーバーモジュールで、本番運用を想定したセキュリティやパフォーマンスを提供します。

### Uvicornを用いたFlaskの起動

Important

結論:Flask をASGIアプリ化すれば Uvicorn で起動できる。

asgi.py(ASGI変換コード)
import os
from asgiref.wsgi import WsgiToAsgi
from app import app as flask_app # Flask APPのインポート

app = WsgiToAsgi(flask_app)
Uvicornでの起動コマンド
uv run uvicorn asgi:app \
    --host 0.0.0.0 \
    --port 8000 \
    --workers 1

## Traefikをリバースプロキシとして利用する

既にTraefikをコンテナで起動して、app-gallery ネットワークに接続している前提とします。

Flaskアプリケーションを起動するための compose.yml には、Traefikのルーティング設定を行うためのラベルを追加します。

compose.yml(Traefikを用いたパスルーティング)
services:
  gallery:
    build:
      context: .
      dockerfile: Dockerfile
    networks: [app-gallery]
    env_file:
      - .env
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${APP_ID}.rule=Host(`${APP_HOST}`) && PathPrefix(`/${APP_ID}`)"
      - "traefik.http.routers.${APP_ID}.entrypoints=web" # 80番ポートから来た通信のみを処理する
      - "traefik.http.services.${APP_ID}.loadbalancer.server.port=${APP_PORT}"
      # - "traefik.http.middlewares.${APP_ID}-strip.stripprefix.prefixes=/${APP_ID}"
      # - "traefik.http.routers.${APP_ID}.middlewares=${APP_ID}-strip"

networks:
  app-gallery:
    external: true

以下でこれらの設定内容を説明します。

  1. ルーティング条件
  • Hostが ${APP_HOST}
  • かつ、パスが /${APP_ID} で始まる

ルクエストだけをこのサービスに流す

"traefik.http.routers.${APP_ID}.rule=Host(`${APP_HOST}`) && PathPrefix(`/${APP_ID}`)"
  1. 転送先ポート

Flaskアプリの起動コマンドで指定したポートをTraefikに伝える

"traefik.http.services.${APP_ID}.loadbalancer.server.port=${APP_PORT}"

## Traefikのルーティングに対応させたミドルウェア設定

Traefikを使うことで、host/${APP_ID} というパスでFlaskアプリケーションにアクセスできるようになります。

一方で、Flaskアプリケーション側ではリクエストのパスが /${APP_ID} から始まることを想定していないため、ルーティングエラーが発生してしまいます。

これらの問題を解決するために、Flaskアプリケーション側でリクエストのパスから /${APP_ID} を取り除くミドルウェアを実装します。

app.py(ミドルウェア部分を抜粋)
app = Flask(__name__)

class PrefixMiddleware:
    def __init__(self, app, prefix=''):
        self.app = app
        self.prefix = prefix.rstrip('/')

    def __call__(self, environ, start_response):
        if self.prefix:
            environ['SCRIPT_NAME'] = self.prefix
            path = environ.get('PATH_INFO', '')
            if path.startswith(self.prefix):
                environ['PATH_INFO'] = path[len(self.prefix):] or '/'
        return self.app(environ, start_response)

APP_ID = os.getenv('APP_ID', '').strip()
if APP_ID:
    app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix='/' + APP_ID)

### PrefixMiddleware でやっていること

Important

WSGIの environ は「リクエスト情報」ですが、ここで2つのキーを調整しています。

  • SCRIPT_NAME : アプリケーションのルートパスを指定するためのキー
  • PATH_INFO : ルーティングの対象となるパスを指定する
  1. PATH_INFO から prefix: /${APP_ID} を剥がす

例:Traefikから来るのが PATH_INFO='/APP_ID/some/path'であれば、middlewareで PATH_INFO='/some/path' に変換する。

これにより、Flaskアプリ上で

app.py
@app.route('/some/path')
def some_path():
    ...

と定義したままであっても、Traefik経由で host/APP_ID/some/path にアクセスしたときに正しくルーティングされるようになります。

  1. SCRIPT_NAME に prefix: /${APP_ID} を設定する

SCRIPT_NAME は、FlaskアプリケーションがURLを生成する際に使用する、アプリケーションのルートパスを指定するためのキーです。

例えば、Flaskアプリケーション内で

app.py
@app.route('/some/path')
def some_path():
    return url_for('some_path')

と定義している場合、url_for('some_path')'/APP_ID/some/path' を返すようになります。

特にHTMLテンプレート内で url_for を使用してパスを生成して、publicなファイルを参照する場合、この設定がないと、生成されるURLが '/static/somefile' のようになってしまい、Traefik経由でアクセスしたときに正しくファイルが参照できなくなります。

### パスの処理はTraefikとFlaskのどちらで行うべき?

Traefikの StripPrefix ミドルウェアを使用して、Traefik側でパスの処理を行う方法もあります。

- "traefik.http.middlewares.${APP_ID}-strip.stripprefix.prefixes=/${APP_ID}"
- "traefik.http.routers.${APP_ID}.middlewares=${APP_ID}-strip"

compose.yaml でコメントアウトしたこれら2行を有効化すると、アプリに届く次点で PATH_INFO から /${APP_ID} が剥がされた状態になります。

一方で、 SCRIPT_NAME は基本的に何もしない限り空文字列のままになるので、PublicファイルへのURL生成(url_for)に問題が発生します。

これを回避するためには、Traefikのmiddleware部分で

# middleware 1: パスから prefix を剥がす
- "traefik.http.middlewares.${APP_ID}-strip.stripprefix.prefixes=/${APP_ID}"

# middleware 2: X-Forwarded-Prefix をつける(リクエストにヘッダー追加)
- "traefik.http.middlewares.${APP_ID}-xfp.headers.customrequestheaders.X-Forwarded-Prefix=/${APP_ID}"

# ルーターに middleware を追加する
- "traefik.http.routers.${APP_ID}.middlewares=${APP_ID}-strip,${APP_ID}-xfp"

という設定を行います。 X-Forwarded-Prefix: /${APP_ID} というヘッダーがリクエストに追加され、これをFlaskアプリで読み込んで SCRIPT_NAME に設定することが可能となります。

この方針であれば、Flaskアプリにおいては、${APP_ID} などの環境変数を意識せずに、通常のルーティング定義やURL生成が可能になります。

ただし、HTTPリクエストのヘッダーを意識しないといけないのが面倒で、Traefik経由でのリクエストか、そうでないのかの区別がつきにくく、ローカルでの開発やテストがやりにくくなる可能性もあります。

前者のFlask側でパスの処理を全て行う方針であれば、${APP_ID} が空かどうかの条件分岐を入れるだけで、ローカル環境と本番環境を同じコードベースで運用できるというメリットもあります。

また、Flaskでのmiddlewareは結局必要となるので、どちらでパスの処理を行うかは好みの問題かなと思います。

## (余談)WSGIとASGIの違い

種類特徴得意なこと
WSGI同期処理に特化したサーバーCPU中心の同期処理。シンプルな構成が可能。
ASGI同期・非同期両方に対応したサーバーAPIサーバーやWebSocket用途。長時間接続やリアルタイム通信が可能。

WSGI の基本思想は、同期モデル (blocking) に基づく処理を行うことです。 1つのリクエストが1スレッド or プロセスで処理され、リクエストの処理が完了するまで次のリクエストは待機します。

WSGIのリクエスト処理モデル
Request

Worker(1つのリクエストがブロックする)

Response

一方、ASGI は非同期モデル (non-blocking) に基づく処理を行うことができます。

ASGIのリクエスト処理モデル
Request

Event Loop

I/O待ち中に他リクエスト処理

Response

複数のリクエストを同時に処理できるため、APIサーバーやWebSocketなどのリアルタイム通信に適しています。