How to serve static files in containerized Django

This question often arises among students to one of the tasks at the very beginning of the course “Middle Python developer» in Yandex Practicum. We asked the mentor on the course Evgeny Morozov to write a detailed answer. We duplicate it here, because we are sure that it will be useful not only to our students.

I have been doing back-end development in Python for over 20 years. At one of my first jobs, I was a system administrator, so the topics of administration and DevОps are also interesting and close to me. In this article, I will answer the question of how to implement the return of static files – such as CSS, js, images – in containerized Django.

There are several ways to solve this problem. For example, more and more modern sites use CDN. Simply put, CDN is a geographically distributed cache for static content to serve it from specially optimized servers located at the minimum possible distance from the client. But for an educational application and even for a small industrial application, resorting to a CDN seems redundant to me. In addition, we may need static files when developing locally or on test servers, and using a CDN in all environments, not just production, will complicate and slow down development. The solution that I will describe is not only suitable for this task – it will be useful to you in other situations.

But first, I’ll tell you about the method that many students resort to. They decide to use a shared volume with statics, which both the Django container has access to collect statics into it with the `manage.py collectstatic` command, and the web server container to serve the statics to the user’s browser. In our case, this is Nginx.

This way seems to me suboptimal. With this approach, the container with Django and the container with the web server become closely related – and must be on the same server, if you do not use, for example, NFS – a network file system. It also seems ugly to me and generates an extra entity – one that is needed only to provide access to files from different containers.

In articles on this subject, I saw the least optimal solution: install Nginx in a container with Django. But then a lot of garbage remains in the final container with Nginx, which inflates the size of the container with useless files that are not required for its operation.

Often, when I’m looking for solutions to problems with docker or any other technology, the first couple of pages of search results are filled with trivial articles like “we’ll tell you how to put a helloworld application in docker” or even articles describing the wrong or suboptimal approach. It seems to me that this is a symptom of a big problem: most authors write blogs for recruiters who will only isolate keywords, but are not able to assess the quality and novelty of the article.

Let’s proceed to the analysis of the solution, which I consider one of the best.

Once I saw an elegant pattern that seems obvious after reading, but which is quite difficult to reach on your own. I have never seen articles that would talk about him in this context. The essence of the approach is to use a multi-stage container assembly. At the first stage, we collect Django and execute the `collectstatic` command, the second stage is built on the basis of the `Nginx` image – only static files from the first stage are copied into it.

For the article I prepared minimal example with Django application, packaged in docker, where Nginx proxy Django requests to the application and also gives the static. Let’s take a closer look at the Dockerfile:

“`

FROM python:3.11-alpine3.18 AS django-static-builder

“`

Here we say that our first stage image should be built from the Python 3.11 image, and we explicitly name it `django-static-builder` so that we can refer to the image in subsequent build stages.

“`

RUN pip install –no-cache-dir poetry==1.5.1

COPY. /app/src

WORKDIR /app/src

“`

We add poetry to the container to install dependencies and copy the application source code into the image. I note that installing poetry in this way is wrong, because in this case, the dependencies of the poetry itself are mixed with the system dependencies and with the dependencies of the project, which can lead to errors both in the work of the poetry itself and in our application. Mistakes like this happen all the time because poetry has quite a few dependencies of its own. But for simplicity, let’s leave it that way for now.

The `–no-cache-dir` option is required, because after the image is built, the temporary file system in which it was built will be deleted. Therefore, it makes no sense to spend time writing the package cache to disk – the next time you run the build, it will no longer exist, and the dependencies will need to be downloaded again. There is a way to cache dependencies when building images, but we will not cover this topic here.

“`

RUN poetry config virtualenvs.create false && \

install poetry –no-interaction –no-ansi

“`

We install the project dependencies, and immediately into the site-packages directory of the system interpreter. This is not usually recommended, but a container is an isolated environment for running exactly one application, so this will not cause problems in it.

“`

RUN ./manage.py collectstatic –noinput

“`

We run a Django command that finds all the application’s static files (in our case, only the built-in admin and django-extensions dependencies) and collects them into one `STATIC_ROOT` directory.

“`

CMD ./manage.py runserver 0.0.0.0:8080

“`

Cutting another corner by using Django’s built-in debug server. In real life, this should not be, a real WSGI server should be running here – such as gunicorn.

“`

FROM nginx:1.25.1-alpine AS front

“`

The second `FROM` instruction in the Dockerfile signals that we are starting to build a new image. It can refer to a previous built image, but in this case we are building an image with Nginx, in which we do not need either Python or Django, but only static files from the first stage of the build.

“`

COPY –from=django-static-builder /app/src/static /data/static

“`

The most important instruction of the second stage of assembly. Here we explicitly specify that we need to copy the directory from the previous build stage to the current one – that is, the Django static collected by the collectstatic command. At the same time, nothing but it falls into this stage from the previous one.

Build and run this configuration from the Django backend with an Nginx reverse proxy in front of it using docker-compose.yml configuration. Consider this configuration:

“`yaml

staticfiles-api:

build:

target: django-static-builder

image:staticfiles-api:develop

“`

The most important instruction here is `target`, which tells you to build a stage called `django-static-builder` and tag it with `staticfiles-api:develop`. Without the `target` statement, the very last stage in the Dockerfile will be built, but for a complete configuration, we need an image with our Django application.

“`yaml

staticfiles-front:

build:

target:front

image:staticfiles-front:develop

“`

This defines the Nginx reverse proxy service. If the latest stage from the Dockerfile is used, then you can not specify it explicitly, but here I decided to leave the name for clarity.

“`yaml

volumes:

– ./conf/nginx.conf:/etc/nginx/nginx.conf:ro

“`

The base nginx image contains a template configuration file that does not know how to proxy requests to our backend, and where the static files are located. Therefore, we replace it with our config, which describes where the backend is located, which needs to be proxyed:

“`

location / {

proxy_pass http://staticfiles-api:8080;

}

“`

And where are the static files:

“`

location /static/ {

root /data;

}

“`

Thus, we got two docker images: one with the application backend, the second with a reverse proxy with static files. They can be deployed to different physical servers, and they will work without the need to create shared volumes. The project infrastructure becomes simpler and more flexible.

This approach to building containers has at least one more use case. Suppose we are not interested in storing statics in a container, because we use, for example, S3 and / or CDN to store and deliver statics. But we are concerned about the size of the container. Now, with a minimum of dependencies and a minimum of code, it is 196 MB. Most likely, as the service develops, its size will reach several gigabytes. On the one hand, it seems like a trifle, but still, even if we ignore the cost of storing dozens of images of several gigabytes each (because CI / CD will generate at least one image per branch, and we need to store them for at least a period of several weeks), then working with images of several gigabytes is simply inconvenient. For example, sometimes you need to run a command in a specific image on a test server or on a developer’s laptop. Downloading a multi-gigabyte image, even over a fast network, creates a noticeable delay.

Let’s make some minor changes to Dockerfile. The first change is to install the poetry dependencies into a virtual environment inside the project:

“`

RUN poetry config virtualenvs.in-project true && \

install poetry –no-interaction –no-ansi

“`

The second change is an additional stage based on the Python image, into which the directory with the project and the virtual environment is copied:

“`

FROM python:3.11-alpine3.18 AS django-api

COPY –from=django-static-builder /app/src /app/src/

WORKDIR /app/src

CMD /app/src/.venv/bin/python manage.py runserver 0.0.0.0:8080

“`

Due to the fact that there is no poetry and its dependencies in the `django-api` stage, the size of the image has decreased by 90 MB. In a real project, you will most likely need a more complex configuration. For example, another image in which poetry will remain is to change project dependencies during development. But you can achieve even more significant savings on the size of the images.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *