Pandora ResearchPandora
Research
RUEN
DevOps

A Dockerfile for a Django project

Pandora ResearchPandora Research
July 11, 20266 min read

In this article we look at an example of creating a Dockerfile for a Django project.

Base image and dependencies

Let's start by choosing an image. If there are no disk-space constraints, I prefer to use the slim-buster image. If there are, it's better to take alpine, but in that case you may need to install additional packages during the container build. So, we pick the Python version we need and add the image to the Dockerfile:

Dockerfile
FROM python:3.10.7-slim-buster

We create a working directory for the application and make it the active one:

Dockerfile
WORKDIR /app

Then we move on to installing Python libraries and dependencies. I use pip for this, keeping the dependencies in a requirements.txt file. We copy the file into the container and install the dependencies:

Dockerfile
COPY ./requirements.txt requirements.txt
RUN pip3 install -r requirements.txt --no-cache-dir --no-warn-script-location

Then we copy the rest of the project files:

Dockerfile
COPY . .

Serving static files with nginx

Serving static files. If static and media files are stored together with Django, you need to set up serving them. When running Django's development server with python manage.py runserver, serving happens automatically, but it's normally not used in production. Let's look at the option of serving static files with nginx.

Let's add nginx to the container, configure it and collect the static files:

Dockerfile
RUN apt-get update
RUN apt-get -y install nginx
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/site.conf /etc/nginx/conf.d/site.conf
RUN python manage.py collectstatic --noinput

The nginx settings in the site.conf file are as follows:

site.conf
server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;

    root /data;
    location @backend {
        proxy_pass http://localhost:8000;
    }

    location / {
        try_files $uri $uri/ @backend;
    }
}

The settings in the nginx.conf file are as follows:

nginx.conf
worker_processes  1;

events {
  worker_connections 1024;
}

http {
  include mime.types;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

  sendfile        on;
  tcp_nodelay     on;
  tcp_nopush      on;

  client_max_body_size 200m;

  gzip on;
  gzip_comp_level 3;
  gzip_min_length 1000;
  gzip_types
        text/plain
        text/css
        application/json
        application/x-javascript
        text/xml
        text/javascript;

  proxy_redirect     off;
  proxy_set_header   Host             $host;

  include conf.d/*.conf;

  server_tokens off;
}

Here we specify that all requests to port 80 are proxied to http://localhost:8000. It also checks whether the files exist by URL at http://localhost:8000. If a file exists, it is served by nginx without a further request to the backend.

This step is optional if the files are stored in S3 storage.

One of the general recommendations when using Docker containers is to run the container's command under a user other than root. But since we need to run nginx under root, we have to keep root.

Port and startup script

Let's tell the container which port should be exposed to the outside, and add a startup script.

Dockerfile
EXPOSE 80

CMD ["./entrypoint.sh"]

We run the startup as a separate script so we can start nginx, run migrations and also start the Django server under gunicorn. The script looks like this:

entrypoint.sh
#!/bin/bash
echo "Starting service..."
/usr/sbin/nginx
python manage.py migrate
gunicorn --bind=0.0.0.0:8000 pandora_research_backend.wsgi

Building and running

That's it. Let's try to build the image with the command:

Terminal
docker build -t django-img-demo .

And then start it:

Terminal
docker run -p 80:80 --name django-demo django-img-demo

You can see the result by entering http://localhost:80 or http://localhost in your browser.

Drawbacks and solutions

As a result we got a container running a Django web server, accessible on the standard port 80. Despite its simplicity, this approach has a number of drawbacks, which is why it's still better not to use it in production. The drawbacks and possible solutions are listed below.

Running the container under root. To solve this, you can split the Django server and nginx into separate containers and then orchestrate them with docker-compose. To share static files, you can mount a shared directory on the server into each of the containers and use it to pass static files between Django and nginx.

You can connect S3 storage and use it to serve static files. There are quite a few Django libraries that let you do this. In that case the need to serve static files disappears, and you can already run the Django web server under a specially created user. The code to add a user is as follows:

Dockerfile
RUN groupadd -r api_group
RUN useradd -d /app -r -g api_group api_user
RUN chown api_user:api_group -R /app
USER api_user
DockerDjangoPython

Got a task for our team?

Tell us about the project — we'll assess it and propose a solution within one business day.

Discuss a project