Serving static assets in a micro-services environment
by Sebastien Mirolo on Sat, 5 Sep 2015While deploying code on a production server can be tricky due to prerequisites, version drift, etc. Serving static assets, commonly called Plain Old Data (POD) has its own set of challenges. Often static assets are "compiled" (gzip, minification), moved around the filesystem and served to a browser by completely different services (ex: nginx) than during development. In practice it means a lot of things can potentially go wrong.
All user-initiated media uploads are directly passed to S3. HTML pages served by nginx, the session proxy or the web application, all use the absolute location (URL) of uploaded media. Those are easy to deal with.
The Web application and session proxy are written in Django. The session proxy implements login, registration and payment processing. It serves HTML pages directly, only forwarding a request to the web application if a set of conditions are matched by the authenticated request user. We thus need to pay attention to relative URLs generated by either the session proxy or web application, make sure a request for them is directed appropriately.
We are looking to support various setup seamlessly:
- Web app on a developer machine
- Web app behind a session proxy while in test mode
- Web app behind a session proxy, itself behind an nginx proxy in full production
Production
Let's start with the most complex services setup. As far as serving static assets, it is the simplest.
Nginx is used in production as a front-end to serve static resources which for some reasons are better kept alongside the rest of the source code (ex: icons, fonts, css, javascript). The purpose here is that all requests for static resources are handled by nginx directly. Only requests for dynamic pages are forwarded.
The process typically involves collecting all static resources and moving
them inside a tree whose root is used as a document_root
for nginx. A web application only has to generate relative URLs
with a known prefix that will get nginx to find the static asset in
document_root
.
$ python manage.py collectstatic
With a session proxy and a web application, we have two collections of static assets. In a multi-tier environment there is a little bit of magic to be done to keep both collections in separate directories, serving them on the same prefix and defaulting to the session proxy collection when a file does not exist in the web application collection.
nginx.conf: /etc/nginx/conf.d/webapp.conf: server { root /var/www/htdocs; location / { try_files webapp/$uri $uri @forward; } }
Developer
In a development setup, we usually don't give much thought to the static assets. Here is a typical Django configuration:
$ cat django_project/urls.py urlpatterns += staticfiles_urlpatterns() $ cat django_project/settings.py APP_STATIC_ROOT = BASE_DIR + '/htdocs/static' if DEBUG: STATIC_ROOT = '' # Additional locations of static files STATICFILES_DIRS = (APP_STATIC_ROOT,) else: STATIC_ROOT = APP_STATIC_ROOT # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django_assets.finders.AssetsFinder' ) STATIC_URL = '/static/' $ cat django_project/templates/index.html {% load static from staticfiles %} <link rel="stylesheet" media="screen" href="{% static 'css/bootstrap.css' %}" /> $ python manage.py runserver
The browser generates a GET request to http://localhost:8000/, the runserver returns a page with a URL to the asset to fetch (ex: /static/css/bootstrap.css). The browser issues another GET request for the css file (http://localhost:8000/static/css/bootstrap.css). Runserver returns the asset file.
There are a few things to note here that will be key later on.
First, because of way Django projects are built, when DEBUG = 1, it is possible the assets will be found in:
-
- static/
- app1/static
- app2/static
STATICFILES_FINDERS = ('django.contrib.staticfiles.finders.AppDirectoriesFinder',)
statement enables.
Test Mode
As the session proxy hints, DjaoDjin builds multi-tier applications. Testing multi-tier hosting on a local machine is a pain if it requires to mangle with sub-domains. It is a lot more convenient to dispatch on a path prefix.
Dynamic page URLs are already prefixed by /app_name/
such
that they get forwarded to the web application. We require the static
assets URLs to also be prefixed by /app_name/
. As a rule:
ALL URLs for the app must be prefixed by /app_name/
(when DEBUG=0)
$ cat web_application/settings.py STATIC_URL = '/%s/static/' % PROJECT_NAME
A developer can then edit an application javascript file, reload the page. The browser requests the javascript resource from the session proxy which forwards the request to the web application. The web application searches its local app directories and sends the modified javascript back.
The pattern works well as long as the HTML page is served by the web
application because the URLs for static assets will be generated correctly.
Problems arise when a developer works on a login page for example.
The HTML login page is served by the session proxy. Any {% static 'css/bootstrap.css' %}
statement in the template will be interpreted in the context of the session
proxy, leading to incorrect resource URLs (i.e. missing
/app_name/
prefix).
Two solutions to this problem are:
- Use pre-generated resource URLs in templates purposed to theme the login experience
- Use a multitier-aware version of the
static
template tags method.
Fixed path resource URLs
The djaodjin-deployutils package implements the first solution.
$ python manage.py package_theme
The package_theme
command will among other things replace
all instances of {% static '...' %}
by "/app_name/..."
. The final output of the
command is a zip file with the collected static assets and templates
that can then be uploaded to djaodjin.com
to theme a site registration, login and payment experience.
multitier-aware {% static %}
The djaodjin-multitier
package implements the second solution. When a {% static '...' %}
statement is executed in the context of the session proxy, The asset URL is
prefixed by the current site theme. Templates must only be modified to load
static from multitier_static instead
of the default Django static implementation.
$ diff -u prev templates/base.html -{% load static %} +{% load static from multitier_static %} <img src="{% static 'img/logo.png' %}" />
More to read
You might also like to read:
- Building CSS/JS static assets and Django
- Mix Vue.js with Django templates
- Integrating Django i18n with Jinja2 and Vue.js
- Multi-tier Implementation in Django
More technical posts are also available on the DjaoDjin blog, as well as business lessons we learned running a SaaS application hosting platform.