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 several 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 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 alongside with the docker-compose
configurations here.