Create a Docker image to store Django static files

Posted by Harald Nezbeda on Fri 29 October 2021

Serving static files is a topic that gets extremely underrated during development, mostly because Django is capable to server static file by itself when using runserver. Newcomers often have difficulties understanding why this is happening and have a hard time when the application needs to run in production. Spending some time on managing static files is recommended.

There are sevral options to solve the issue such as serving them via web server, an S3 bucket or whitenoise. In this article I will focus on creating a docker image that hosts the static files. The image can be used to to start a container directly or inside a docker-compose setup or as a pod in kubernetes. After that the files will be forwarded to the browser directly or by using a reverse proxy.

Collect the static files

Similar to a container for running the python application, we need bring the code inside the container and make sure all dependencies are installed. If the code is available locally it can be pipped in the container by using COPY. Another good approach is to clone the repository when building the image.

FROM python:3.10-slim

RUN apt-get update && apt-get install git -y
ARG GIT_HASH="master"
WORKDIR /usr/src/app
RUN git clone https://github.com/nezhar/snypy-backend .
RUN git checkout $GIT_HASH
RUN pip install pip -U
RUN pip install --no-cache-dir -r requirements.txt

The GIT_HASH is build-time variables that can be a branch, a tag or a commit id which can be provided via build args or it defaults to master. The next step is to go inside the app directory and run the collectstatic command (in my case also some env vars are required because django-environ is used, and some variables have no defaults).

WORKDIR /usr/src/app/snypy
ENV SECRET_KEY=static
ENV ALLOWED_HOSTS=static
ENV DATABASE_URL=static
ENV CORS_ORIGIN_WHITELIST=static
ENV REGISTER_VERIFICATION_URL=static
ENV RESET_PASSWORD_VERIFICATION_URL=static
ENV REGISTER_EMAIL_VERIFICATION_URL=static
RUN python manage.py collectstatic --noinput

Extend the container to serve files

At this stage the container can be extended to also serve the files by installing a webserver such as apache or nginx. By using the python:3.10-slim image the size of the container is about 300MB, opposed to almost 900MB if using the default python:3.10 image. But in our case this is way to much just for serving some css, javascript or font files. A lot of modules and python packages are not even required. Luckily we can improve this by using multistage builds.

First step is to add an alias to the primary image used in the build: FROM python:3.10-slim as build

After the collecstatic setup a new FROM statement can be added and the artifacts from the previous step can be copied by referencing the alias.

FROM nginx:1.21-alpine
COPY --from=build /static /usr/share/nginx/html

The final result will be an image containing all the static files that have been collected by django which in my case has about 38MB. Even better, by making use of the nginx:1.21-alpine image, configuring the webserver or changing the CMD instruction is not even required, as the defaults work as indented.

Full dockerfile

Bellow is a snapshot from the release of this article:

# https://hub.docker.com/_/python
FROM python:3.10-slim as build

RUN apt-get update && apt-get install git -y
ARG GIT_HASH="master"
WORKDIR /usr/src/app
RUN git clone https://github.com/nezhar/snypy-backend .
RUN git checkout $GIT_HASH
RUN pip install pip -U
RUN pip install --no-cache-dir -r requirements.txt

WORKDIR /usr/src/app/snypy
ENV SECRET_KEY=static
ENV ALLOWED_HOSTS=static
ENV DATABASE_URL=static
ENV CORS_ORIGIN_WHITELIST=static
ENV REGISTER_VERIFICATION_URL=static
ENV RESET_PASSWORD_VERIFICATION_URL=static
ENV REGISTER_EMAIL_VERIFICATION_URL=static
RUN python manage.py collectstatic --noinput


# https://hub.docker.com/_/nginx
FROM nginx:1.21-alpine
COPY --from=build /static /usr/share/nginx/html

You can find the dockerfile in the snypy-docker repository along side with the docker-compose configurations here.