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:
FROM python:3.10.7-slim-busterWe create a working directory for the application and make it the active one:
WORKDIR /appThen 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:
COPY ./requirements.txt requirements.txt
RUN pip3 install -r requirements.txt --no-cache-dir --no-warn-script-locationThen we copy the rest of the project files:
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:
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 --noinputThe nginx settings in the site.conf file are as follows:
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:
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.
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:
#!/bin/bash
echo "Starting service..."
/usr/sbin/nginx
python manage.py migrate
gunicorn --bind=0.0.0.0:8000 pandora_research_backend.wsgiBuilding and running
That's it. Let's try to build the image with the command:
docker build -t django-img-demo .And then start it:
docker run -p 80:80 --name django-demo django-img-demoYou 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:
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