Securing a Containerized Django Application with Let's Encrypt
How do I set up an SSL Certificate for a Django application?
In this tutorial, we'll look at how to secure a containerized Django app running behind an HTTPS Nginx proxy with Let's Encrypt SSL certificates.
This tutorial builds on Dockerizing Django with Postgres, Gunicorn, and Nginx. It assumes you understand how to containerize a Django app along with Postgres, Nginx, and Gunicorn.
Nowadays you simply can't go to production with your application running over HTTP. Without HTTPS, your site is less secure and trustworthy. With Let's Encrypt, which simplifies the process of obtaining and installing SSL certificates, there's simply no excuse anymore not to have HTTPS.
Django on Docker Series:
- Dockerizing Django with Postgres, Gunicorn, and Nginx
- Securing a Containerized Django Application with Let's Encrypt (this tutorial!)
- Deploying Django to AWS with Docker and Let's Encrypt
To follow this tutorial you will need:
- a domain name
- a running Linux virtual machine with Docker and Docker Compose installed where your app will be deployed (AWS EC2, Google Compute Engine, DigitalOcean, Linode are all viable options)
Need a cheap domain to practice with? Several domain registrars have specials on '.xyz' domains. Alternatively, you can create a free domain at Freenom.
There are a number of different ways to secure a containerized Django app with HTTPS. Arguably, the most popular approach is to add a new service to your Docker Compose file that utilizes Certbot for issuing and renewing SSL certificates. While this is perfectly valid, we'll take a slightly different approach and use the following projects:
- nginx-proxy - used to automatically build your Nginx proxy configuration for running containers where each container is treated as a single virtual host
- acme-companion - used to issue and renew Let's Encrypt SSL certificates for each of the containers proxied by nginx-proxy
Together, these projects simplify the management of your Nginx configuration and SSL certificates.
Another option is to use Traefik instead of Nginx. In short, Traefik works with Let's Encrypt to issue and renew certificates. For more, check out Dockerizing Django with Postgres, Gunicorn, and Traefik.
When the app is deployed for the first time, you should follow these two steps to avoid issues with certificates:
- Start by issuing the certificates from Let's Encrypt's staging environment
- Then, when all is running as expected, switch to Let's Encrypt's production environment
To protect their servers, Let's Encrypt enforces rate limitations on their production validation system:
- 5 validation failures per account, per hostname, per hour
- 50 certificates may be created per domain per week
If you make a typo in your domain name or in a DNS entry or anything similar, your request will fail, which will count against your rate limit, and you'll have to attempt to issue a new certificate.
To avoid being rate limited, during development and testing, you should use Let's Encrypt's staging environment for testing their validation system. The rate limits are much higher on the staging environment, which is better for testing. Just be aware that the issued certificates in staging are not trusted publicly, so once everything is working, you should switch over to their production environment.
First, clone down the contents from the GitHub project repo:
$ git clone https://github.com/testdrivenio/django-on-docker django-on-docker-letsencrypt $ cd django-on-docker-letsencrypt
This repository contains everything that you need to deploy a Dockerized Django app minus the SSL certificates, which we'll be adding in this tutorial.
First, to run the Django app behind an HTTPS proxy you'll need to add the SECURE_PROXY_SSL_HEADER setting to settings.py:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
In this tuple, when
X-Forwarded-Proto is set to
https the request is secure.
You'll also need to update
CSRF_TRUSTED_ORIGINS inside settings.py:
CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS").split(" ")
It's time to configure Docker Compose.
Let's add a new Docker Compose file for testing purposes called docker-compose.staging.yml:
version: '3.8' services: web: build: context: ./app dockerfile: Dockerfile.prod command: gunicorn hello_django.wsgi:application --bind 0.0.0.0:8000 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles expose: - 8000 env_file: - ./.env.staging depends_on: - db db: image: postgres:13.0-alpine volumes: - postgres_data:/var/lib/postgresql/data/ env_file: - ./.env.staging.db nginx-proxy: container_name: nginx-proxy build: nginx restart: always ports: - 443:443 - 80:80 volumes: - static_volume:/home/app/web/staticfiles - media_volume:/home/app/web/mediafiles - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - /var/run/docker.sock:/tmp/docker.sock:ro depends_on: - web acme-companion: image: nginxproxy/acme-companion env_file: - ./.env.staging.proxy-companion volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - certs:/etc/nginx/certs - html:/usr/share/nginx/html - vhost:/etc/nginx/vhost.d - acme:/etc/acme.sh depends_on: - nginx-proxy volumes: postgres_data: static_volume: media_volume: certs: html: vhost: acme:
Add a .env.staging.db file for the
POSTGRES_USER=hello_django POSTGRES_PASSWORD=hello_django POSTGRES_DB=hello_django_prod
Change the values of
POSTGRES_PASSWORDto match your user and password.
We already looked at the
db services in the previous tutorial, so let's dive into the
Databases are critical services. Adding additional layers, such us Docker, adds unnecessary risk in production. To simplify tasks such as minor version updates, regular backups, and scaling, it's recommended to use a managed service like AWS RDS, Google Cloud SQL, or DigitalOcean's Managed Database.
Nginx Proxy Service
For this service, the nginx-proxy project is used for generating a reverse proxy configuration for the
web container using virtual hosts for routing.
Be sure to review the README on the nginx-proxy repo.
Once up, the container associated with
nginx-proxy automatically detects containers (in the same network) that have the
VIRTUAL_HOST environment variable set and dynamically updates its virtual hosts configuration.
Go ahead and add a .env.staging file for the