Set up Django static files (Images, CSS, JavaScript) with WSGI using WhiteNoise

Problem

You want to use the same WSGI server to serve Django's dynamically generated pages and Django's static files, but want to optimize WSGI to handle static files.

Solution

Do pip install whitenoise to install WhiteNoise. Next, add the whitenoise.middleware.WhiteNoiseMiddleware middleware class to your settings.py file in the second position -- after SecurityMiddleware. To enable compression and cache-friendly files with WhiteNoise add the STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' variable to your settings.py file. To create compression and cache-friendly files with WhiteNoise ensure you run the python manage.py collectstatic command once you add the STATICFILES_STORAGE variable. You can add support for Brotli compression in WhiteNoise by installing the Brotli python package: pip install brotlipy.

How it works

In all the previous recipes that touched on the process of serving Django static files, I never failed to mention the optimal approach to serve these types of files was through a dedicated web server or service like Amazon S3, because it was wasteful to use the same pipeline to process dynamic and static content.

In the cases where I did provide a single server instance solution for dynamic and static content, such as Single Apache site configuration to run static resources on same instance and Single Nginx site configuration to run static resources on same instance, the solution was to re-direct requests for static files at the web server level before they hit the WSGI layer used for dynamic Django content.

But what if you could introduce a 'smart way' to deal with static files in WSGI and just let it handle every request. In this manner you could forget about setting up a separate web server or service like Amazon S3 to handle static files. Enter WhiteNoise.

WhiteNoise is a Python package that integrates with any WSGI-compliant application, to process static files with a focus on optimization (e.g. compression and caching). For Django applications, WhiteNoise integrates into the standard static file management process and ties into Django's STATIC_ROOT to pick up static files and the collectstatic command.

Note Ensure you know the ABCs of Django static file management

This recipe assumes you already know about Django's static file management. If the STATIC_ROOT concept or collectstatic command are unfamiliar to you, I strongly recommend you first read the recipe Set up static web page resources -- Images, CSS, JavaScript otherwise some of following techniques may not work as described.

As a first step install the WhiteNoise package: pip install whitenoise. Once you install WhiteNoise you're ready to move onto the configuration. Listing 1 illustrates the minimum set up needed to run WhiteNoise with a Django project.

Listing 1 - Django settings.py minimum setup for WhiteNoise


MIDDLEWARE_CLASSES = (
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
)

As you can see in listing 1, the only thing you need to enable WhiteNoise is to add the whitenoise.middleware.WhiteNoiseMiddleware class to the MIDDLEWARE_CLASSES variable in settings.py. Just ensure you add the WhiteNoise middleware class in the second position -- after the SecurityMiddlware class -- as Django middleware class position is relevant as described in the recipe Use Django middleware to modify requests & responses.

Once you do this, WhiteNoise is ready to work with Django. But you may ask yourself, what is it the WhiteNoise middleware does ? It overtakes the handling of static resources from STATIC_URL and resolves them to the STATIC_ROOT folder. In this sense, it allows Django to work with static files from a path in the main application (e.g. STATIC_URL='/static/') with any web server or with DEBUG=False -- a behavior that Django provides out-of-the-box but only for its built-in web server and when DEBUG=True.

Hey! But didn't you say serving static files from a path in the main application was wasteful and bad practice ? Well I did say that, but that was without the use of WhiteNoise. Where WhiteNoise really sets itself apart from other deployment techniques for static files is when you enable compression and caching.

Enable compression and caching of Django static files with WhiteNoise

Using compression and caching for static files is really where you have the most to gain in terms of performance & speed vs. optimizing web server pipelines. With compression you ensure static files use a reduced footprint that translates into faster transit times and less bandwidth consumption. And with a proper caching strategy, you ensure static files are never re-requested unless there's a good reason for it, a process that can be further enhanced if you use a CDN ('Content Delivery Network') to serve as a smarter middleman cache to serve static content.

Compression and caching are not unknown techniques, web servers like Apache & Nginx, as well as services like Amazon S3, all support both compression and caching in one way or another. But realistically speaking compression and caching are difficult techniques to get right, as not all clients/browsers can handle compression and determining caching strategies can depend on the type of file, to name a few of the difficulties. In this case, WhiteNoise lets you integrate compression and caching for Django static files in a few simple steps, without the need to fiddle with web server or service configurations that fall outside the scope of application development.

Listing 2 illustrates a configuration to enable the compression and caching of Django static files with WhiteNoise.

Listing 2 - Django settings.py setup for WhiteNoise with compression and caching


MIDDLEWARE_CLASSES = (
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
)

STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'

As you can see in listing 2, the only difference between listing 1 is that listing 2 declares the STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' variable. Setting the STATICFILES_STORAGE variable has the purpose of overriding the default Django static file management behavior. In the case of whitenoise.storage.CompressedManifestStaticFilesStorage, it provides the mechanism to generate compressed and cache-friendly files each time you run collectstatic, as well as provide the integration for Django template references (e.g. {% load static %} <img src="{% static 'images/logo.png' %}">) to use these compressed and cache-friendly files created by WhiteNoise.

For example, if among the source static files you have the favicon.ico file, when you run the collectstatic command with WhiteNoise it not only copies this file to the STATIC_ROOT folder, it also creates a cache-friendly version (e.g favicon.8cdbbe4bdca6.ico) and compressed versions for both files (e.g. favicon.ico.gz, favicon.8cdbbe4bdca6.ico.gz). The same thing happens for other source static files (e.g. bootstrap.min.js gets copied to STATIC_ROOT along with the newly created bootstrap.min.js.gz, bootstrap.min.ba847811448e.js and bootstrap.min.ba847811448e.js.gz).

The generation of compressed, cache-friendly & original file versions makes it easy to support all clients/browsers (e.g. those that don't support or use compression), as well as limit the overhead of generating optimized file versions on-the-fly (e.g. Apache web server's mod_deflate module). So the next issue becomes, how does Django generate links to these compressed and cache-friendly static files instead of using the original file names ? This is also done by the STATICFILES_STORAGE WhiteNoise class.

When you define the STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' variable, this also tells Django to generate all template links (e.g. {% load static %} {% static %} statements) with WhiteNoise's internal logic. This means the template snippet {% load static %} <script src="{% static '/bootstrap/js/bootstrap.min.js' %}"></script>, gets converted to <img src="/static/bootstrap/js/bootstrap.min.ba847811448e.js"></script>. So when a rendered Django template is sent out to browsers, its references for static resources are aligned with the same static file names generated by the collectstatic command.

Note Brotli compression support in WhiteNoise

Besides the gzip compression format, WhiteNoise also supports the up and coming Brotli compression format.

To enable Brotli with WhiteNoise you need to install the brotlipy package (e.g. pip install brotlipy). Once you install the package and run the collectstatic command again, Django/WhiteNoise also generates static files with this format (e.g. files with a .br extension).

Note that because the Brotli compression format is relatively new, only a few browser like Firefox support it. However, WhiteNoise is capable of detecting which browsers request it and serve it only in these cases.