How can I run Django on a subpath on Google Cloud Run with load balancer?

I'll preface by noting that I have a system set up using Google Cloud Run + Load Balancer + IAP to run a number of apps on https://example.com/app1, https://example.com/app2, etc, and up to now I've only deployed Streamlit apps this way. The load balancer is directing traffic to each app in Cloud Run according to subpath (/app1, ...), and I used the --server.baseUrlPath=app2 option of streamlit run with no problems.

Now I'm trying to get a 'hello, world' Django 4.1.5 app running on https://example.com/directory, and I can't seem to get the routes right trying differing suggestions here, here, and here.

The Dockerfile ends with

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

First, the canonical solution.

I added FORCE_SCRIPT_NAME = "/directory" in settings.py.
Here's example_dir/urls.py:

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

and here's directory/urls.py:

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

Visiting https://example.com/directory returns

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']

That Request URL is surprising and weird.

Adding either USE_X_FORWARDED_HOST = True or SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') or both(per the nixhive.com reference) did not affect the result.

Second, the ugly solution.

If I change example_dir/urls.py to

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

then visiting https://example.com/directory works properly, but https://example.com/directory/admin returns but is unstyled:
unstyled django admin page
This is obviously due to the <head> having hrefs like /static/admin/....

Changing STATIC_URL in settings.py to "directory/static/" fixed the hrefs but I see several errors on the console like

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.

An orthogonal variable

I can't really do this in prod, but as an experiment I changed the Docker CMD to

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

This does not affect the results of the FORCE_SCRIPT_NAME solution, but it fixes the problem with the ugly solution. Specifically, the MIME type errors are gone and the admin login page looks normal.

I don't think script name has anything to do with what you are trying to achieve. You need to have a WSGI app wrapping your Django app so that you can inject a path before.

There is an example on how to achieve this on Gunicorn https://github.com/benoitc/gunicorn/blob/master/examples/multiapp.py

Here is a modified version for your use case

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()
Back to Top