## 想定する基本的なディレクトリ構成
.
├─ templates
│ ├─ index.html
│ └─ result.html
├─ Dockerfile
├─ app.py # Flaskアプリケーションのエントリーポイント
├─ asgi.py # ASGIアプリ化を行うためのファイル
├─ compose.yml
├─ pyproject.toml
└─ uv.lockapp.py にはFlaskアプリケーションのコードが記述されています。
これらは uv で管理されているプロジェクトとしています。
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起動方法
本番環境では、Gunicorn や uWSGI といったWSGIサーバー、あるいは Uvicorn といったASGIサーバーを使用してFlaskアプリケーションを起動します。
これらはPython向けの本番用サーバーモジュールで、本番運用を想定したセキュリティやパフォーマンスを提供します。
### Uvicornを用いたFlaskの起動
Important
結論:Flask をASGIアプリ化すれば Uvicorn で起動できる。
import os
from asgiref.wsgi import WsgiToAsgi
from app import app as flask_app # Flask APPのインポート
app = WsgiToAsgi(flask_app)uv run uvicorn asgi:app \
--host 0.0.0.0 \
--port 8000 \
--workers 1## Traefikをリバースプロキシとして利用する
既にTraefikをコンテナで起動して、app-gallery ネットワークに接続している前提とします。
Flaskアプリケーションを起動するための 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以下でこれらの設定内容を説明します。
- ルーティング条件
- Hostが
${APP_HOST} - かつ、パスが
/${APP_ID}で始まる
ルクエストだけをこのサービスに流す
"traefik.http.routers.${APP_ID}.rule=Host(`${APP_HOST}`) && PathPrefix(`/${APP_ID}`)"- 転送先ポート
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 = 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: ルーティングの対象となるパスを指定する
PATH_INFOから prefix:/${APP_ID}を剥がす
例:Traefikから来るのが PATH_INFO='/APP_ID/some/path'であれば、middlewareで PATH_INFO='/some/path' に変換する。
これにより、Flaskアプリ上で
@app.route('/some/path')
def some_path():
...と定義したままであっても、Traefik経由で host/APP_ID/some/path にアクセスしたときに正しくルーティングされるようになります。
SCRIPT_NAMEに prefix:/${APP_ID}を設定する
SCRIPT_NAME は、FlaskアプリケーションがURLを生成する際に使用する、アプリケーションのルートパスを指定するためのキーです。
例えば、Flaskアプリケーション内で
@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 プロセスで処理され、リクエストの処理が完了するまで次のリクエストは待機します。
Request
↓
Worker(1つのリクエストがブロックする)
↓
Response一方、ASGI は非同期モデル (non-blocking) に基づく処理を行うことができます。
Request
↓
Event Loop
↓
I/O待ち中に他リクエスト処理
↓
Response複数のリクエストを同時に処理できるため、APIサーバーやWebSocketなどのリアルタイム通信に適しています。