Django/Python

Serving static files with Django

December 4th, 2021

How do you serve your static and media files with Django? Here are a few use cases and popular ways to do it.

As you start your Django journey with a new project, Django's runserver helpfully handles your static files for you. But the documentation emphatically tells you not to do that in production use: "This method is grossly inefficient and probably insecure, so it is unsuitable for production".

So, what's the best way to do it?

Static and media files

Before we begin, here's a quick refresher for what we mean by static and media files in the context of Django, as they are handled differently in some cases.

By static files we mean files that are part of your project (committed to your code repository) and are served as-is to your visitors. These may be images, CSS files, JavaScript files, PDFs, audio or video files - anything that's served as is, but is built-in to your project.

By media files we mean files that are uploaded by your users and afterward served as is. A typical example are photos uploaded by your users.

Local and cloud storage

Both static files and media files can be stored either locally, on the same server that hosts the Django project, or somewhere else, for example on a cloud storage service.

Back in the day when most projects were deployed on a dedicated or virtual server, most files were stored locally. Today, if using services or tools that treat the deployed server as an immutable (read-only) image, this is no longer possible. Examples of this approach are Heroku and docker. In this case, storing media files locally is impossible, requiring the use of some kind of cloud storage.

Serving via a web server

If your Django instance sits behind a web server such as Nginx or Apache and your files are stored locally, the webserver can serve them directly.

Assuming your static files are located (collected) in /var/app/static (so that's your STATIC_ROOT in Django settings) and served under /static/ prefix (that's your STATIC_URL in Django), here's an example Nginx config to serve them:

location /static/ {
    root /var/app;
}

If you'd also want to serve media files, assuming MEDIA_URL is /media/ and MEDIA_ROOT is /var/app/media, you'd add another block:

location /media/ {
    root /var/app;
}

As an aside: since Nginx appends the URI path to the document root, it's useful to have the STATIC_ROOT end in STATIC_URL and MEDIA_ROOT end in MEDIA_URL - it makes configuration simpler.

For Apache, the configuration would look something like this:

DocumentRoot /var/app;

Alias /static /var/app/static;
Alias /media /var/app/media;

<Directory /var/app>
  Deny from all
</Directory>
<Directory /var/app/static>
  Allow from all
</Directory>
<Directory /var/app/media>
  Allow from all
</Directory>

Note that both examples are minimal and ignore things like compression, setting expiration headers, and so on.

Serving from cloud storage

If you're using cloud storage such as Amazon S3, you probably use the excellent django-storages. It makes serving static files directly from the cloud service easy. In your Django settings file, you just set your STATICFILES_STORAGE (for static files) and DEFAULT_FILE_STORAGE (for media files) to your preferred backend.

Example for Amazon S3:

STATICFILES_STORAGE = 'storages.backends.s3boto3.S3StaticStorage'
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'

When you call manage.py collectstatic as part of your deployment procedure, the storages engine will upload all static files to S3. Media files will be uploaded to S3 when you try to save them server-side.

Again, note that this is a minimalistic example - see django-storages documentation for detailed installation instructions and all the options.

Serving static files via whitenoise

If you're serving your media files from a cloud service, you still have the option to serve static files locally. You may want to do that to simplify deployment or rollback procedure or for some other reason.

If you're behind a web server you can use it to serve static files as explained previously. But if you're using docker or deployed to a platform like Heroku, you may not be able to do that. Instead, you can serve them directly from Django, using whitenoise.

Enabling whitenoise is as simple as adding it to the list of middlewares in Django settings:

MIDDLEWARE = [
  'whitenoise.middleware.WhiteNoiseMiddleware',
  # ...
]

Note the placement of middleware matters - be sure to read the docs.

Compression and caching

Both django-storages and whitenoise support compression and manifest storage for your content.

Compressing your static files where it makes sense can dramatically decrease their size and thus load time of your pages. CSS and JavaScript files are good candidates for compression, while images, audio, and video files are already compressed.

If you enable manifest storage, the collectstatic step will rename the files to a unique name. For example, script.js might be renamed to script.f9f204dfa.js, where f9f204dfa is a manifest, or checksum, of the file contents. The manifest changes each time the file contents change, meaning each change to a file will generate a new, unique, name.

This, in turn, means we can tell the browser (and a CDN if you use one) to cache files forever. When you deploy a new version of your project that changes the file, a new file name will be used and the browser (or a CDN) will know to fetch the new file.

An important caveat here is that you must always access the static files using the {% static %} template tag. The template tag will rewrite the name to use the actual (current) manifest. If you hardcode path like script.js in your template, it will not be found

Use with CDN

All of the above approaches can be used irrespective of whether you're using a content delivery network (CDN) or not. If you do use CDN, using manifest storage will help you even more.

A real-world example

If you'd like to see a real-world example of serving files with django-storages or whitenoise, try our Django project builder. As you change the relevant options in the builder on the left-hand side, check the generated Django settings on the right (be sure to select settings/base.py for previewing to see the changes).