自作のflaskアプリを動かそうとしたのですが、サブディレクトリで運用しようとしたらハマりました。
しかも、意外と情報がない。あれー、結構ありうる問題だと思うのだけどなぁ。
というわけで、メモしておくことにします。
環境
- ubuntu24.04
- apache2-2.4.58
- python 3.12.3
- Flask 3.1.0
- gunicorn 23.0.0
まずは、動かしてみる。
とりあえず、apacheの設定。
<VirtualHost *:80>
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
<Location />
require all granted
</Location>
</VirtualHost>
flaskのソースコード、実際は違うけど、仮に以下のようにします。
# coding: utf-8
from flask import Flask, url_for, make_response
app = Flask(__name__)
@app.route('/')
def index():
url = url_for('hoge', name='aaa')
body = f'''<html>
<body>
<p><a href="{url}">{url}</a></p>
</body>
'''
return make_response(body, 200)
@app.route('/hoge/<name>')
def hoge(name):
url = url_for('index')
return f'''<html>
<body>
<p>name = {repr(name)}.</p>
<p><a href="{url}">{url}</a.</p>
</body>
</html>
'''
application = app
あとは、apacheのmod_proxyなどを有効にしたり、gunicornなどでサーバを立てたりして、動かします。
まぁ、今回はそこはポイントじゃないんで、端折ります。
サブディレクトリでの運用、…で失敗する。
さて、ドキュメントルートは別の静的ドキュメントで置いときたい、とか、複数のflaskアプリを動かしたい、とか、flaskアプリを http://example.com/myapp/
というようにサブディレクトリに置いときたい、と思うことがあるでしょう。いや、あったんです。
というわけで、apacheの設定を以下のように修正します。
<VirtualHost *:80>
DocumentRoot /var/www/html
ProxyPass /myapp/ http://127.0.0.1:8000/
ProxyPassReverse /myapp/ http://127.0.0.1:8000/
<Location /myapp/>
require all granted
</Location>
</VirtualHost>
ちょっと余談になりますが、「 /myapp/
」の最後の「 /
」は、付けた方がいいと思います。
付けないと、「 http://example.com/myapp
」と「 http://example.com/myapp/
」の両方でアクセスできてしまいます。
いや、それ自体はまぁ別にいいかもしれないけど、そこでHTMLの中で「 ./hoge.html
」というリンクを貼った場合、前者は「 http://example.com/hoge.html
」へのリンク、後者は「 http://example.com/myapp/hoge.html
」となってしまい、ブレが発生してしまいます。
閑話休題。
上記の設定でapacheを動かすと、indexページは正しく表示されますが、 hoge/aaa
へのリンクが正しく貼れません。
というのも、flaskの url_for
はエントリポイント、具体的には /
から始まるパスを返すからです。
つまり、 url_for('hoge', name='aaa')
は「 /hoge/aaa
」を返すので、開くページは「 http://example.com/hoge/aaa
」になってしまいます。
まぁ、flaskアプリとしては、自分がリバースプロキシとして呼ばれているかどうかは知る由もないので、しかたないですね。
解決策、その1
まず解決策の一つとして、全てのリンクを相対パスに書き換える方法です。
と言っても、一つ一つ手で書き換えてしまうと url_for
を使っていたメリットが無くなってしまいますので、ここは代わりとなる relpath_for
という関数を作ってしまいましょう。
from flask import request, url_for
def relpath_for(entry, **kwargs):
parts = request.path[1:].split('/')
return ('/'.join(['..'] * (len(parts) - 1)) if len(parts) > 1 else '.') + url_for(entry, **kwargs)
ご覧のとおり、パスの階層に従って頭に「 ..
」を付け加えているだけです。
もっとも、これだと同じディレクトリにあっても ..
が加えられてしまうので、もしそれが気に入らなければもう少し工夫が必要です。
また、以下を参照に app.jinja_env.globals['relpath_for'] = relpath_for
としておけば、テンプレート内でも使えそうです。
解決策、その2
一応、こちらがflask公式の回答(?)です。
上記に従い、 wsgi.py
を以下のように修正します。
app = Flask(__name__)
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
(何となく、今回の問題だけで言えば x_prefix=1
だけでいいかもしれませんが。)
しかし、これだけでは足りません。
上記のサイトにも、
HTTPサーバは、本当の値をFlaskアプリケーションへ渡すためにX-Forwarded-ヘッダーを設定するべきです。
と書かれています。
そこで、apacheの設定ファイルも以下のように書き換えます。
<VirtualHost *:80>
DocumentRoot /var/www/html
ProxyPass /myapp/ http://127.0.0.1:8000/
ProxyPassReverse /myapp/ http://127.0.0.1:8000/
<Location /myapp>
require all granted
RequestHeader set X-Forwarded-Prefix /myapp/
</Location>
</VirtualHost>
なお、 RequestHeader
を使うには、apacheの mod_headers
モジュールが必要です。
ubuntuの場合には、以下のコマンドで有効にしましょう。
sudo a2enmod headers