Running Django on a subpath
Most of us been there, only some of us succeeded. I can say that I officially belong to the latter group now. After hours of research, I came to a somewhat crazy solution in terms of how to run Django applications on a subpath. Let me get some context around this, and we’ll be moving at a fast pace.
Despite the trends and advantages of nginx over Apache, I still had to stick with Apache due to being unable to affect the production server configuration. Because of the tremendous amount of system dependencies, the Django project in question lies within a Vagrant box (which essentially has an identical setup to the production machine). During development, the goal was to start this Vagrant box which runs the Django app on a subpath, forward the port being used by Apache inside the box to the host machine, twist plot here, the host machine is actually another VM running in KVM, which exposes this forwarded port to the outside world on a subpath too. So it actually is, subpath concatenation! Or something like that…
+---------------+ | | | OUTSIDE WORLD | | | +---------------+ | | +-------------------+ | ROUTER forwarding | | port 5983 to the | | internal KVM VM | +-------------------+ | | +----------------------------+ | KVM VM on port 5983 and | | subpath /vbox001 proxying | | to localhost:8080(vagrant) | +----------------------------+ | | +-----------------------+ | Vagrant box serving | | Django app on port 80 | | on subpath /mydjapp | +-----------------------+
So let’s start from the outside, going towards the inner Django app. The KVM virtual machine is the host of the Vagrant box. In my Vagrantfile, I specified to forward port 80 (from the Vagrant box), to port 8080 on the host machine (KVM VM):
config.vm.network :forwarded_port, host: 8080, guest: 80
The KVM virtual machine is running Apache with the following virtual host dedicated to proxying requests from port 5983, and location
/vbox001, to port 8080 (the Vagrant box).
<VirtualHost *:5983> ProxyPreserveHost On <Location "/vbox001"> ProxyPass http://localhost:8080 ProxyPassReverse http://localhost:8080 ProxyPassReverseCookieDomain djangoapp.domain external.domain RequestHeader set X-SCRIPT-NAME /vbox001 RequestHeader set X-SCHEME http </Location> </VirtualHost>
It’s just a regular reverse proxy config, but what’s interesting is that all incoming requests will get two additional headers attached here by Apache,
X-SCHEME. These headers must be forwarded explicitly, otherwise we would lose the information they contain, and the server towards which we’re proxying requests would have no idea that the request came in through the
/vbox001 subpath, nor about whether it’s running on http or https.
ProxyPassReverseCookieDomain param is present here, so that it changes the cookie domain for all requests, from the internal to the external domain. Cookies would otherwise use the
ServerName specified by the inner Apache server (which config we will see below). In my case,
external.domain was just an IP address, but that depends on whether you have a registered domain name or not, so change it according to your setup.
Now that we have the setup of our outer Apache server, let’s go inside the Vagrant box, and see the config of the Apache instance running there:
<VirtualHost *:80> # just to note, this is the domain to which cookies will be assigned to # normally, and the above mentioned `ProxyPassReverseCookieDomain` # changes it to the external domain. Otherwise the browser would ignore # them, because the domains wouldn't match (the browser sees the # application running on `external.domain`) ServerName djangoapp.domain ServerAdmin webmaster@localhost ProxyPreserveHost On <Location "/mydjapp"> ProxyPass http://127.0.01:8000 ProxyPassReverse http://127.0.0.1:8000 RequestHeader append X-SCRIPT-NAME /mydjapp RequestHeader edit X-SCRIPT-NAME ", /" "/" </Location> Alias /static/ /var/www/mydjapp/staticroot/ <Directory /var/www/mydjapp/staticroot/> Order allow,deny Allow from all </Directory> </VirtualHost>
So once again, this is another reverse proxy, which will forward all incoming requests on path
/mydjapp, to the locally running (gunicorn, uwsgi or whatever server you use). But the real fun starts here, with the following two directives:
RequestHeader append X-SCRIPT-NAME /mydjapp RequestHeader edit X-SCRIPT-NAME ", /" "/"
They are kinda very hackish, but I had no better idea, if someone has a better solution please enlighten me. Remember the outer Apache config already attached two additional headers that we use to track the subpath and scheme under which we serve our app. Because we’re using another subpath here, we have to concatenate these two paths. So when the request got here, it contained a header that looked like:
Then the first directive:
RequestHeader append X-SCRIPT-NAME /mydjapp
appends the second subpath to it, so we get:
X-SCRIPT-NAME /vbox001, /mydjapp
However, the append docs state that: “The request header is appended to any existing header of the same name. When a new value is merged onto an existing header it is separated from the existing header with a comma. This is the HTTP standard way of giving a header multiple values.” So we need to remove that comma and whitespace with:
RequestHeader edit X-SCRIPT-NAME ", /" "/"
So at the end we get:
For actual explanation how exactly the
RequestHeader directive works, check out: http://httpd.apache.org/docs/2.2/mod/mod_headers.html#requestheader
At the end, we need to set up a quirky wsgi.py file:
import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mydjapp.settings.dev") from django.core.wsgi import get_wsgi_application _application = get_wsgi_application() def application(environ, start_response): script_name = environ.get('HTTP_X_SCRIPT_NAME', '') if script_name: environ['SCRIPT_NAME'] = script_name path_info = environ['PATH_INFO'] if path_info.startswith(script_name): environ['PATH_INFO'] = path_info[len(script_name):] scheme = environ.get('HTTP_X_SCHEME', '') if scheme: environ['wsgi.url_scheme'] = scheme return _application(environ, start_response)
Within the wsgi file, we wrap the
application function with another function, which essentially extracts the headers we forwarded (
HTTP_X_SCHEME), and updates the
PATH_INFO wsgi environment variables. Straight from the docs, these two variables are used for:
The initial portion of the request URL’s “path” that corresponds to the application object, so that the application knows its virtual “location”. This may be an empty string, if the application corresponds to the “root” of the server.
The remainder of the request URL’s “path”, designating the virtual “location” of the request’s target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash.
So we set
/vbox001/mydjapp and make sure that
PATH_INFO does not contain
/vbox001/mydjapp, only the eventual path that comes after
/vbox001/mydjapp, for instance
Your Django settings file does NOT need to use the
FORCE_SCRIPT_NAME config param at all. Just make sure, in order to serve the static files correctly, the actual
STATIC_URL param must be set to use the absolute url:
STATIC_URL = 'http://external.domain:5983/vbox001/static/'
And finally, the URL config also doesn’t need any special care, so the following:
would be totally valid and shall work.
That would be all, your Django app should be up and running on: http://external.domain:5983/vbox001/mydjapp/