Serving static assets in a micro-services environment

by Sebastien Mirolo on Sat, 5 Sep 2015

While 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.

Terminal
$ 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.

Terminal
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:

Terminal
$ 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
That's what a 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)
Terminal
$ 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:

  1. Use pre-generated resource URLs in templates purposed to theme the login experience
  2. Use a multitier-aware version of the static template tags method.

Fixed path resource URLs

The djaodjin-deployutils package implements the first solution.

Terminal
$ 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.

Terminal
$ 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:

More technical posts are also available on the DjaoDjin blog, as well as business lessons we learned running a SaaS application hosting platform.

by Sebastien Mirolo on Sat, 5 Sep 2015


Receive news about DjaoDjin in your inbox.

Bring fully-featured SaaS products to production faster.

Follow us on