Deploying a Django application in Docker with Nginx

Today, we are going to deploy a simple Django application using Gunicorn and Nginx. We will also configure it to work with TLS.

Prerequisites

  1. A Django application. You can use the code that I will be using for this blog post. This was built using Python 3.5.1.

Project Setup

The root of my application has the following files and directories:

config/- A collection of configuration files that will be used during the build process.

Dockerfile- Commands for building the hello_earth container.

docker-compose.yml- Commands for running multiple containers.

hello_earth/- Directory containing the Django application code.

LICENSE- Do whatever you want with this code!

Create TLS Certificate and Key

I created a self-signed TLS certificate and keyfile and placed them in config/nginx/certs. To do this:

openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out localhost.crt -keyout localhost.key

OpenSSL will prompt you to answer a few questions. It is important that “Common Name” matches the domain name of the server that this will be deployed on. In our case, Common Name is “localhost”.

You will need to obtain a certificate signed by a valid certificate authority, such as Let’s Encrypt, for production environments.

Configure Nginx

Create a file at config/nginx/nginx.conf with the following contents:

upstream web {
ip_hash;
server web:443;
}
# Redirect all HTTP requests to HTTPS
server {
listen 80;
server_name localhost;
return 301 https://$server_name$request_uri;
}
server { # Pass request to the web container
location / {
proxy_pass https://web/;
}
listen 443 ssl;
server_name localhost;
# SSL properties
# (http://nginx.org/en/docs/http/configuring_https_servers.html)
ssl_certificate /etc/nginx/conf.d/certs/localhost.crt;
ssl_certificate_key /etc/nginx/conf.d/certs/localhost.key;
root /usr/share/nginx/html;
add_header Strict-Transport-Security "max-age=31536000" always;
}

Configure the Docker files

Dockerfile

# Use Python 3.6.3 as a base imageFrom python:3.6.3# Prevent Docker from outputting to stdoutENV PYTHONBUFFERED 1# Make a directory called "code" which will contain the source code. This will be used as a volume in our docker-compose.yml fileRUN mkdir /code# Add the contents of the hello_earth directory to the code directoryADD ./hello_earth /code# Set the working directory for the container. I.e. all commands will be based out of this directoryWORKDIR /code# Install all dependencies required for this project. the trusted-host flag is useful if you are behind a corporate proxy.RUN pip install --trusted-host pypy.org --trusted-host files.pythonhosted.org -r requirements.txt

docker-compose.yml

Our application will consist of two containers: the container that we built using Dockerfile and an Nginx image. The overall structure will look like:

version: '3'services:
web:
...
nginx:
...

Now, we will break down each service.

web

web:
build: .
command: bash -c "python manage.py makemigrations && python manage.py migrate && gunicorn --certfile=/etc/certs/localhost.crt --keyfile=/etc/certs/localhost.key hello_earth.wsgi:application --bind 0.0.0.0:443"
container_name: hello_earth
env_file:
- ./config/web/web-variables.env
volumes:
- ./code:/src
- ./config/nginx/certs/:/etc/certs
expose:
- "443"

As I mentioned earlier, the working directory was set to /code, which contains the source code of hello_earth. This makes it easier to run the commands for building the application.

python manage.py makemigrations && python manage.py migrate

The above command looks for changes to the database models and then applies them. Our application does not have a use for a database currently. I plan to write another blog post showing how we can integrate this application with a containerized database.

gunicorn --certfile=/etc/certs/localhost.crt --keyfile=/etc/certs/localhost.key hello_earth.wsgi:application --bind 0.0.0.0:443

Gunicorn is a production-ready WSGI server for running Python web applications. The above command is telling Gunicorn to use the certificate and key that we put in /config/nginx/certs and bind the application to port 443 (https). Please note that this is the container’s port, not the server that Docker is hosted on.

env_file:
- ./config/web/web-variables.env

The environmental variables that we defined in config/web/web-variables.env are set using the above command. This prevents us from having to set the DJANGO_SECRET_KEY within the actual code.

volumes:
- ./code:/src
- ./config/nginx/certs/:/etc/certs

Bind the /code directory from the image we built using Dockerfile to /src on this container.

Take the certificate and key files from /config/nginx/certs and put them in /etc/certs in this container.

expose:
- "443"

Expose port 443 so that this container can be accessed by dependent containers at https://web/443.

nginx

nginx:
image: nginx:latest
container_name: ng
ports:
- "443:443"
- "80:80"
volumes:
- ./config/nginx/:/etc/nginx/conf.d
depends_on:
- web

We are going to be using the latest available Nginx container. If the build fails, you can always specify a specific version (e.g. nginx:1.17.4).

This container will be exposed to the outside world via ports 80 and 443. This is so that the application can be accessed through traditional HTTP or HTTPS. We redirect all HTTP requests to HTTPS in our nginx.conf file.

The Nginx configuration file that we created will be stored in /etc/nginx/conf.d/nginx.conf.

This container depends on the “web” container that we built above. This is why nginx.conf proxies https://web:443.

Time to Deploy

Our docker-compose.yml should look like the following:

version: '3'services:
web:
build: .
command: bash -c "python manage.py makemigrations && python manage.py migrate && gunicorn --certfile=/etc/certs/localhost.crt --keyfile=/etc/certs/localhost.key hello_earth.wsgi:application --bind 0.0.0.0:443"
container_name: hello_earth
env_file:
- ./config/web/web-variables.env
volumes:
- ./code:/src
- ./config/nginx/certs/:/etc/certs
expose:
- "443"
nginx:
image: nginx:latest
container_name: ng
ports:
- "443:443"
- "80:80"
volumes:
- ./config/nginx/:/etc/nginx/conf.d
depends_on:
- web

First, we need to start the Docker daemon.

In a terminal, run the following command:

docker-compose up --build

Get some coffee — this may take a minute. Once completed, you should see the following:

[INFO] Starting gunicorn 19.9.0
[INFO] Listening at: https://0.0.0.0:443 (10)
[INFO] Using worker: sync
[INFO] Booting worker with pid: 13

In your browser, go to either https://localhost or http://localhost and you should see the following top-of-the line innovation:

“Not Secure” is because my certificate is self-signed.

Conclusion

You have now successfully built and deployed a Django application using Docker! In the near future, I plan to expand on this by incorporating a database and automatically generating Let’s Encrypt signed certificates using certbot. Don’t hesitate to drop a note in the comments if you have questions or have found a b̶u̶g̶ feature.

I’m a web developer who primarily works with Django, Node and Angular. I have a strong interest in doing more Devopsy things in my day-to-day.