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
- 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.
- You are probably going to want to have Python installed if you want to test your application locally.
- Docker and Docker Compose
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:
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.