0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

flaskでリバースプロキシで、サブディレクトリで運用する際の注意

Posted at

自作の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のソースコード、実際は違うけど、仮に以下のようにします。

wsgi.py
# 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 を以下のように修正します。

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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?