Как я могу запустить Django на подпути в Google Cloud Run с балансировщиком нагрузки?

В начале отмечу, что у меня есть система, настроенная с использованием Google Cloud Run + Load Balancer + IAP для запуска ряда приложений на https://example.com/app1, https://example.com/app2 и т.д., и до сих пор я развертывал таким образом только приложения Streamlit. Балансировщик нагрузки направляет трафик на каждое приложение в Cloud Run согласно подпути (/app1, ...), и я использовал опцию --server.baseUrlPath=app2 streamlit run без проблем.

Сейчас я пытаюсь получить приложение Django 4.1.5 'hello, world', работающее на https://example.com/directory, и я не могу получить правильные маршруты, пробуя различные предложения здесь, здесь, и здесь.

Dockerfile заканчивается на

CMD exec poetry run gunicorn --bind 0.0.0.0:${PORT} --workers 1 --threads 8 --timeout 0 example_dir.wsgi:application

Сначала каноническое решение.

Я добавил FORCE_SCRIPT_NAME = "/directory" в settings.py.
Вот example_dir/urls.py:

urlpatterns = urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("directory.urls")),
]

и вот directory/urls.py:

urlpatterns = [
    path("", views.hello, name="hello"),
]

Посещение https://example.com/directory возвращается

Page not found (404)
Request Method: GET
Request URL:    http://example.com/directory/directory
Using the URLconf defined in example_dir.urls, Django tried these URL patterns, in this order:

1. admin/
2. [name='hello']

Этот URL запроса удивительный и странный.

Добавление либо USE_X_FORWARDED_HOST = True, либо SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https'), либо обоих (согласно ссылке на nixhive.com) не повлияло на результат.

Второе, уродливое решение.

Если я изменю example_dir/urls.py на

urlpatterns = [
    path("directory/admin/", admin.site.urls),
    path("directory/", include("directory.urls")),
]

после посещения https://example.com/directory работает правильно, но https://example.com/directory/admin возвращается, но без стилей:
unstyled django admin page
Это очевидно из-за того, что <head> имеет hrefs как /static/admin/....

Замена STATIC_URL в settings.py на "directory/static/" исправила hrefs, но я вижу несколько ошибок на консоли, таких как

Refused to apply style from 'https://example.com/directory/static/admin/css/base.css' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.

Ортогональная переменная

Я не могу сделать это в prod, но в качестве эксперимента я изменил Docker CMD на

CMD exec poetry run python manage.py runserver 0.0.0.0:${PORT}

Это не влияет на результаты решения FORCE_SCRIPT_NAME, но исправляет проблему с некрасивым решением. В частности, исчезают ошибки типа MIME, и страница входа в систему администратора выглядит нормально.

Я не думаю, что имя скрипта имеет какое-то отношение к тому, чего вы пытаетесь достичь. Вам нужно иметь приложение WSGI, обертывающее ваше приложение Django, чтобы вы могли вводить путь перед.

На сайте Gunicorn https://github.com/benoitc/gunicorn/blob/master/examples/multiapp.py

есть пример того, как этого добиться.

Вот модифицированная версия для вашего случая использования

from routes import Mapper
from example_dir.wsgi import application as app1

class Application(object):
    def __init__(self):
        self.map = Mapper()
        self.map.connect('app1', '/directory', app=app1)

    def __call__(self, environ, start_response):
        match = self.map.routematch(environ=environ)
        if not match:
            return self.error404(environ, start_response)
        return match[0]['app'](environ, start_response)

    def error404(self, environ, start_response):
        html = b"""\
        <html>
          <head>
            <title>404 - Not Found</title>
          </head>
          <body>
            <h1>404 - Not Found</h1>
          </body>
        </html>
        """
        headers = [
            ('Content-Type', 'text/html'),
            ('Content-Length', str(len(html)))
        ]
        start_response('404 Not Found', headers)
        return [html]

app = Application()
Вернуться на верх