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:
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()