, , ,

Django Docker Deployment with HTTPS using Letsencrypt

In this guide, I’ll show you how to enable HTTPS on a Django app that’s deployed using Docker.

You can find the full source code for this tutorial project here: LondonAppDeveloper/django-docker-deployment-with-https-using-letsencrypt

Watch this tutorial.

Prerequisites

In order to follow this guide, you’ll need the following:

Setup Docker

The first step is to add Docker to our project so we can use it to create our Django app.

Start by creating a requirements.txt file which will be used to list all the Python requirements our project needs:

Django==4.0.5
uWSGI==2.0.20

(Code diff)

Create a Dockerfile which will be used for our Django application.

FROM python:3.10-alpine3.16

ENV PYTHONUNBUFFERED 1

COPY requirements.txt /requirements.txt
RUN apk add --upgrade --no-cache build-base linux-headers && \
    pip install --upgrade pip && \
    pip install -r /requirements.txt

COPY app/ /app
WORKDIR /app

RUN adduser --disabled-password --no-create-home django

USER django

CMD ["uwsgi", "--socket", ":9000", "--workers", "4", "--master", "--enable-threads", "--module", "app.wsgi"]

(Code diff)

Create docker-compose.yml which will be used as our development docker service configuration.

version: '3.9'

services:
  app:
    build:
      context: .
    command: sh -c "python manage.py runserver 0.0.0.0:8000"
    volumes:
      - ./app:/app
    ports:
      - 8000:8000

(Code diff)

We’ll put our Django project in a directory called app/ in the root of our project. We need to create an empty directory now in order to build our Dockerfile which will be used to run the Django CLI.

Create our Django project

Now that Docker is setup, we can create our Django project by running the following:

docker-compose run --rm app sh -c "django-admin startproject app ."

(Code diff)

This will create a new Django project in our app directory.

Now run docker-compose up to start the development server locally.

Once done, you should be able to navigate to http://127.0.0.1:8000 and see the Django launch page.

Django development server splash page.

Create Django app

Next we’ll configure Django and create an app which we can use to test our deployed code.

Run the following command to create a new app:

docker-compose run --rm app sh -c "python manage.py startapp home"

Since we’re creating a simple app for testing, you can remove the following boilerplate files from our new app:

  • migrations/
  • admin.py
  • models.py
  • tests.py

Open settings.py and add the new home app to the INSTALLED_APPS list:

INSTALLED_APPS = [
    ...
    'home',
]

(Code diff)

Create a new file (and subdirectories) at app/home/templates/home/index.html and add the following contents:

<html>
  <head>
    <title>Django with HTTPS</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>This is a Django app with HTTPS enabled!</p>
  </body>
</html>

(Code diff)

Update views.py to look like this:

from django.shortcuts import render

def index(request):
    return render(request, "home/index.html")

(Code diff)

Now we need to wire this view up to a URL. Do this by editing app/app/urls.py to look like this:

from django.contrib import admin
from django.urls import path

from home import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index),
]

(Code diff)

Now if you refresh the page, it should look like this:

Of course, HTTPS isn’t actually enabled yet – that’s what we’ll do in the rest of this tutorial.

Add NGINX Docker Image

The next step is to add an NGINX reverse proxy to the project.

The first time we run it, it will handle the following initialisation steps:

  • Generate DH Parameters which will be stored in a volume (see What’s the purpose of DH Parameters? to learn what this is for)
  • Handle the acme challenge of HTTP for initialising our certificate

These are only required the first time we deploy our project to a new server.

After the first run, it will then handle the following:

  • Redirect HTTP requests to HTTPS
  • Handle Django static files
  • Forward requests to uWSGI

Because our NGINX image will have numerous configuration files, we’ll create a new subdirectory for them at docker/proxy/.

First we’ll create our default config template at docker/proxy/nginx/default.conf.tpl:

server {
    listen 80;
    server_name ${DOMAIN} www.${DOMAIN};

    location /.well-known/acme-challenge/ {
        root /vol/www/;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

(Code diff)

This will be the first configuration file used to handle the initial setup of our project.

We put .tpl at the end of the file name because this is a template file which we’ll use to generate our actual config at runtime. This is so we can replace all instances of ${DOMAIN} with the domain name we want to use for our project.

From the top down, this is what the file does:

  • Sets up a server block which is required to define our NGINX server configuration
  • Listen on port 80 – the default port for HTTP
  • Add a location block for /.well-known/acme-challenge/ that serves data from /vol/www/ – this will serve a one time password generated by the certbot which needs to be accessible on the internet for letsencrypt to give us a certificate (see Challenge Types for more details)
  • Redirect all other requests to https

Now create a second file at docker/proxy/nginx/default-ssl.conf.tpl and add the following:

server {
    listen 80;
    server_name ${DOMAIN} www.${DOMAIN};

    location /.well-known/acme-challenge/ {
        root /vol/www/;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen      443 ssl;
    server_name ${DOMAIN} www.${DOMAIN};

    ssl_certificate     /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;

    include     /etc/nginx/options-ssl-nginx.conf;

    ssl_dhparam /vol/proxy/ssl-dhparams.pem;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location /static {
        alias /vol/static;
    }

    location / {
        uwsgi_pass           ${APP_HOST}:${APP_PORT};
        include              /etc/nginx/uwsgi_params;
        client_max_body_size 10M;
    }
}

(Code diff)

This is the config which will be used once our SSL/TLS certificates are initialised.

The first block is the same as the default.conf.tpl file we created previously. This is so we can continue to redirect HTTP to HTTPS and handle the acme challenge for certificate renewals.

The second server block does the following:

  • Listens on port 443 with SSL
  • Sets the server name as configured by the DOMAIN variable
  • Configure our ssl_certificate and ssl_certificate_key which will be set by certbot and mapped to /etc/letsencrypt/ via a volume later on
  • Include our options-ssl-nginx.conf file which we’ll add in a minute
  • Set our ssl_dhparam file which we’ll be adding to our startup script later on
  • Adds a header which enables Strict-Transport-Security which is a way to tell the client’s browser to always use HTTPS for our domain and subdomains
  • Handles requests to /static and serves them from the /vol/static directory – this is a way to handle Django static files (we don’t cover that in this guide, but you may want to add it later)
  • Adds a location block for / which will take the rest of the requests and forward them to our uWSGI service running on APP_HOST and APP_PORT

Add a new file at docker/proxy/nginx/options-ssl-nginx.conf with the following contents:

# Taken from:
# https://github.com/certbot/certbot/blob/1.28.0/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf

ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

(Code diff)

As the comment at the top of the file suggests, which is taken from the official certbot configuration. It contains the configuration options that are needed to use SSL/TLS certificates retrieved by certbot.

Create a new file at docker/proxy/nginx/uwsgi_params and add the following contents:

uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_ADDR $server_addr;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;

(Code diff)

This file is taken from the official uWSGI docs. It’s used to pass header parameters from NGINX to our Django app.

Add a new file called docker/proxy/run.sh with the following content:

#!/bin/bash

set -e

echo "Checking for dhparams.pem"
if [ ! -f "/vol/proxy/ssl-dhparams.pem" ]; then
  echo "dhparams.pem does not exist - creating it"
  openssl dhparam -out /vol/proxy/ssl-dhparams.pem 2048
fi

# Avoid replacing these with envsubst
export host=\$host
export request_uri=\$request_uri

echo "Checking for fullchain.pem"
if [ ! -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
  echo "No SSL cert, enabling HTTP only..."
  envsubst < /etc/nginx/default.conf.tpl > /etc/nginx/conf.d/default.conf
else
  echo "SSL cert exists, enabling HTTPS..."
  envsubst < /etc/nginx/default-ssl.conf.tpl > /etc/nginx/conf.d/default.conf
fi

nginx -g 'daemon off;'

(Code diff)

This is a bash script which does the following:

  • Check if /vol/proxy/ssl-dhparams.pem exists, if not then run openssl dhparam to generate it – this is required the first time we run the proxy
  • Check if /etc/letsencrypt/live/${DOMAIN}/fullchain.pem exists, if not then copy the default.conf.tpl – this will cause the server to run without SSL so it can serve the acme challenge
  • Set host and require_uri variables to prevent them being overwritten with blank values in the configs
  • If the fullchain.pem file does exist, copy the default-ssl.conf.tpl – this will cause the server to enable SSL

Now we can create docker/nginx/Dockerfile and add the following:

FROM nginx:1.23.0-alpine

COPY ./nginx/* /etc/nginx/
COPY ./run.sh /run.sh

ENV APP_HOST=app
ENV APP_PORT=9000

USER root

RUN apk add --no-cache openssl bash
RUN chmod +x /run.sh

VOLUME /vol/static
VOLUME /vol/www

CMD ["/run.sh"]

(Code diff)

This will build an image with the following:

  • Based on the nginx image
  • Copies the config files and scripts from our project folder to the docker image
  • Set default configuration values for APP_HOST and APP_PORT which are used to configure which uWSGI service requests will be forwarded too
  • Install openssl which is required to generate the dh params and bash which is used to run our run.sh script
  • Make our run.sh file executable
  • Define two values: /vol/static which can be used to map static files from our Django app to the proxy, and /vol/www which will serve our acme challenge
  • Set the command to /run.sh so we don’t need to specify it when running containers from our image

Add certbot Docker Image

Next we are going to add our certbot Docker image which will be used to retrieve our first certificate and then handle renewals.

Create a file (and directory) at docker/certbot/certify-init.sh, and add the following contents:

#!/bin/sh

# Waits for proxy to be available, then gets the first certificate.

set -e

# Use netcat (nc) to check port 80, and keep checking every 5 seconds
# until it is available. This is so nginx has time to start before
# certbot runs.
until nc -z proxy 80; do
    echo "Waiting for proxy..."
    sleep 5s & wait ${!}
done

echo "Getting certificate..."

certbot certonly \
    --webroot \
    --webroot-path "/vol/www/" \
    -d "$DOMAIN" \
    --email $EMAIL \
    --rsa-key-size 4096 \
    --agree-tos \
    --noninteractive

(Code diff)

See the inline comments for what the script does.

The certbot certonly command is how we run certbot and tell it to get us a certificate. The flags do the following:

  • certonly means get the certificate only (don’t try and install it into the web server)
  • --webroot tells it to obtain a certificate by writing to the wrbroot directory of an already running server
  • --webroot-path is used to specify the path of the webroot
  • -d is used to specify the domain we want to get the certificate for – we’ll set the value as an environment variable later
  • --email needs to be set to a valid email address for renewal to work – we’ll also set this as an environment variable later
  • --rsa-key-size is the size of the rsa key (4096 is better than 2048)
  • --agree-tos confirms that we agree to the Let’s Encrypt Subscriber Agreement
  • --noninteractive tells certbot we are running this as a script, so we don’t want it to prompt for any inputs

Now we’ll add our Dockerfile for certbot.

Create a new file at docker/certbot/Dockerfile and add the following:

FROM certbot/certbot:v1.27.0

COPY certify-init.sh /opt/
RUN chmod +x /opt/certify-init.sh

ENTRYPOINT []
CMD ["certbot", "renew"]

(Code diff)

This Dockerfile does the following:

  • Bases our image from certbot so we can get the certbot executable as well as netcat
  • Adds our certify-init.sh script which we created above
  • Sets the entrypoint to an empty array – this is to override the default certbot entrypoint so we can run our script easier
  • Set the default command to certbot renew

Add deployment Docker Compose

Next we’ll add a specific Docker Compose configuration that will be used for deployment.

Open app/app/settings.py and change it to this:

import os
# ...

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "setmeinprod")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.environ.get("DJANGO_DEBUG", 0)))

ALLOWED_HOSTS = [] if DEBUG else os.environ.get("DJANGO_ALLOWED_HOSTS").split(',')

# ...

(Code diff)

The purpose of this change is to make some of our settings configurable from environment variables which we can set in our Docker Compose config.

Specifically we do the following:

  • Import os which is the build-in library that’s used to retrieve values from environment variables
  • Pull the SECRET_KEY value from the DJANGO_SECRET_KEY setting so we can avoid keeping the real secret key in our code
  • Set the DEBUG setting from DJANGO_DEBUG – because environment variables come in as strings, we need to confirm it to an integer and then a bool (so 1 = True and 0 = False)
  • Set ALLOWED_HOSTS from DJANGO_ALLOWED_HOSTS which will be used to set our custom domain name

Update docker-compose.yml to add an environment variable to enable debug mode, so it looks like this:

version: '3.9'

services:
  app:
    build:
      context: .
    command: sh -c "python manage.py runserver 0.0.0.0:8000"
    volumes:
      - ./app:/app
    ports:
      - 8000:8000
    environment:
      - DJANGO_DEBUG=1

(Code diff)

Now create a new file in the root of the project called docker-compose.deploy.yml with the following contents:

version: "3.9"

services:
  app:
    build:
      context: .
    restart: always
    environment:
      - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
      - DJANGO_ALLOWED_HOSTS=${DOMAIN}

  proxy:
    build:
      context: ./docker/proxy
    restart: always
    depends_on:
      - app
    ports:
      - 80:80
      - 443:443
    volumes:
      - certbot-web:/vol/www
      - proxy-dhparams:/vol/proxy
      - certbot-certs:/etc/letsencrypt
    environment:
      - DOMAIN=${DOMAIN}

  certbot:
    build:
      context: ./docker/certbot
    command: echo "Skipping..."
    environment:
      - EMAIL=${ACME_DEFAULT_EMAIL}
      - DOMAIN=${DOMAIN}
    volumes:
      - certbot-web:/vol/www
      - certbot-certs:/etc/letsencrypt/
    depends_on:
      - proxy

volumes:
  certbot-web:
  proxy-dhparams:
  certbot-certs:

(Code diff)

This defines our Docker Compose deployment with the services:

  • An app service which will run our Django app
  • A proxy service which runs our reserve proxy and opens port 80 (HTTP) and 443 (HTTPS)
  • A certbot service which will handle retrieving and renewing our certificate

We also create the following volumes:

  • certbot-web – this is used to share that acme challenge with our nginx web server, so it can be made accessible to fulfil the request.
  • proxy-dhparams – this is used to store our dhparams file which is generated by our proxy the first time we run it.
  • certbot-certs – this will hold our certificates that are generated by certbot, and make them available to our nginx proxy.

As you see we pull in some environment variables using the ${VAR_NAME} syntax in our config. This can be used to pull values from a file called .env which we will store on our server.

We’ll create a sample of the file so we can easily copy it and update the values at deploy time.

Create a new file called .env.sample and add the following contents:

DJANGO_SECRET_KEY=secretkey123
ACME_DEFAULT_EMAIL=email@example.com
DOMAIN=example.com

(Code diff)

The above file is just a template with values which we must change upon deployment.

Create a server

Next we need to create a new server to run our service.

Our server will require the following:

  • Accessible via SSH (22), HTTP (80) and HTTPS (443)
  • Has Docker and Docker Compose installed
  • Has crontab – used to automate renewal

I’ll be using AWS in the steps below, but feel free to use whatever host you want as you can create a server with the following specs.

First, login to the AWS console at https://console.aws.amazon.com/

We’ll be using SSH to connect to our server, so before we create it, navigate to EC2 > Key Pairs, and import your public key for SSH authentication (teaching SSH is out of scope for this tutorial).

Screenshot of importing an SSH key pair in the AWS console.
Importing an SSH key in AWS.

Next, head back to the EC2 Dashboard and select Launch Instance:

Screenshot of the Launch instance button on the AWS EC2 dashboard

On the Launch an instance page, enter the following:

  • Name: django-https (or whatever name you prefer)
  • Application and OS Images: Amazon Linux 2 AMI (HVM)
  • Architecture: 64-bit (x86)
  • Instance type: t2.micro – this should be enough for this demo, but you may want to use a larger machine for a real deployment
  • Key pair name: Select your key pair for SSH auth
  • Network settings: Allow SSH, HTTP and HTTPS traffic
  • Configure storage: I recommend at least 25GB as Docker needs to pull a bunch of base images
Screenshot of the Launch instance page with the previously listed configuration.

Note that AWS may charge you for creating this virtual machine. It’s your responsibility to review and accept all costs associated with following this guide. If in doubt, see the AWS official documentation or contact their support.

Now select Launch instance. Then choose View all instances to view the newly created instance.

After the instance is launched, you should be able to select it and view it’s Public IPv4 DNS:

Screenshot of the Public IPv4 DNS address for a virtual machine in the AWS console.

Use the public IPv4 DNS to connect to the server via SSH:

ssh ec2-user@<address>

After you’ve connected, install Docker with the following command:

# Install Docker
sudo yum update -y
sudo amazon-linux-extras install -y docker
sudo systemctl enable docker.service
sudo systemctl start docker.service
sudo usermod -aG docker ec2-user

# Install Docker Compose
wget https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)
sudo mv docker-compose-$(uname -s)-$(uname -m) /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# Install Git
sudo yum install -y git

This does the following:

  • Updates the yum package manager repos
  • Installs docker
  • Enables the docker service so it auto starts
  • Starts the docker setvice
  • Adds the ec2-user to the docker group so it can run containers
  • Installs docker-compose
  • Installs Git which we’ll need to clone our project

To confirm that everything is installed correctly, run:

[ec2-user@ip-172-31-40-166 ~]$ docker --version
Docker version 20.10.13, build a224086
[ec2-user@ip-172-31-40-166 ~]$ docker-compose --version
docker-compose version 1.29.2, build unknown

Then, type exit to disconnect from SSH, and then reconnect again (this is so the group change gets applied to the user)

Now run docker run hello-world to ensure you can run containers. If it worked, it should look something like this:

[ec2-user@ip-172-31-40-166 ~]$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete 
Digest: sha256:13e367d31ae85359f42d637adf6da428f76d75dc9afeb3c21faea0d976f5c651
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

[ec2-user@ip-172-31-40-166 ~]$ 

Deploying from GitHub

Now we have a server to deploy to, we need to find a way to get our code from GitHub onto the server.

There are various ways to do this. For this guide, we’ll create a special SSH key on our server called a “Deploy Key”, and configure our GitHub project to permit cloning with this key.

Ensure you are connected to your server via SSH and run the following:

ssh-keygen -t ed25519 -C "GitHub Deploy Key"

When prompted for the key path, leave it as default.

When prompted for the passphrase, leave this empty (or set a passphrase that you will remember to use for every deployment).

[ec2-user@ip-172-31-40-166 ~]$ ssh-keygen -t ed25519 -C "GitHub Deploy Key"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/ec2-user/.ssh/id_ed25519): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/ec2-user/.ssh/id_ed25519.
Your public key has been saved in /home/ec2-user/.ssh/id_ed25519.pub.
The key fingerprint is:
SHA256:oC7ffxpnSmIwUWkdsTUwo58+vdw6YTYbhWJKgTJSL68 GitHub Deploy Key
The key's randomart image is:
+--[ED25519 256]--+
| ..  ..o*+o      |
|. o...+..= .     |
| ..oo.o..  .     |
|   o o.oo.. .    |
|    =. oS. .     |
|   o o.. .*      |
|  E . o =o+=     |
|   o o o B+o     |
|    . ..+o+o.    |
+----[SHA256]-----+
[ec2-user@ip-172-31-40-166 ~]$ 

Now run cat ~/.ssh/id_ed25519.pub to print the public key, and copy it’s contents to your keyboard:

Screenshot of the printed SSH public key

With the public key copied to your keyboard, head over to your GitHub project and choose Settings > Deploy Keys:

Deploy keys option under GitHub project settings.

Select Add deploy key and give it a title before pasting the contents of your key. Then click Add key.

Screenshot of adding a new deploy key in GitHub

You may be prompted to re-enter your GitHub password.

After that, your key should be added:

Now go to your Github project’s Code tab, and choose Code > SSH before copying the clone URL:

Clone URL for GitHub project

Now head back to the server and run:

git clone <paste url here>

This will clone your project to your server.

You can now use cd to navigate to the cloned project:

[ec2-user@ip-172-31-40-166 ~]$ ls
django-docker-deployment-with-https-using-letsencrypt
[ec2-user@ip-172-31-40-166 ~]$ cd django-docker-deployment-with-https-using-letsencrypt/

(Yours may look a bit different depending on what you called your Git repo)

Setup DNS

Unfortunately, Let’s Encrypt will not allow you to register a certificate for the ephemeral hostname that AWS provides for new EC2 instances.

As a result, you’ll need to register a custom domain name to access your services.

There are numerous ways to do this, and DNS is a complex topic which is out of scope for this guide.

The easiest option is to register your own domain using Route 53 in AWS, but NOTE that there will be a charge for this (even in the AWS free tier). It’s usually around $10 per year, but it depends on the name you wish to register.

Once you have a Hosted zone in AWS, you can add a new subdomain by creating a CNAME record like below:

Screenshot of adding a CNAME record.

Note that the value above is the hostname of my EC2 instance. You should, of course, change this to the hostname of your instance if you want it to work.

The above record will point app.aws.londonapp.dev to the server with the hostname provided in the value field.

Once you hit Create records, it may take a few minutes for the DNS change to propagate and become usable.

Configure app

Now you need to create a configuration file, this is done by copying the .env.sample to .env and changing the values:

cp .env.sample .env
vi .env

Set the values as per your project.

Note, the DOMAIN value must be the domain name you will use to access your project on because this is used to set your ALLOWED_HOSTS value.

For example, my config looks like this:

Screenshot of my .env configuration

Getting the first certificate

Now the project is cloned, you can run the following command to generate the first SSH certificate:

docker-compose -f docker-compose.deploy.yml run --rm certbot /opt/certify-init.sh

It may take a while to wait for the proxy. This is because the first time we run the service, the proxy will generate our dhparams file, which can take a couple of minutes (this will be saved in a volume so they don’t need to be created again.

If it’s successful, it should look like this:

Successful initialisation of certificates.

Now, run the following to stop and start the service:

docker-compose -f docker-compose.deploy.yml down
docker-compose -f docker-compose.deploy.yml up

This will restart all services and serve our application via HTTPS.

Once running, you should be able to navigate to your project at your registered domain name via HTTPS.

Screenshot of deployment with validate certificate.

Handling renewals

The steps above will create an initial certificate for our project. However, the certificate will only be valid for three months, so you’ll need to run the renew command before then.

To renew, we can run:

docker-compose -f docker-compose.deploy.yml run --rm certbot sh -c "certbot renew"

If you run this now, you should see something like this:

[ec2-user@ip-172-31-36-103 django-docker-deployment-with-https-using-letsencrypt]$ docker-compose -f docker-compose.deploy.yml run --rm certbot sh -c "certbot renew"
Creating django-docker-deployment-with-https-using-letsencrypt_certbot_run ... done
Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/app.aws.londonapp.dev.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate not yet due for renewal

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The following certificates are not due for renewal yet:
  /etc/letsencrypt/live/app.aws.londonapp.dev/fullchain.pem expires on 2022-10-01 (skipped)
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

It’s best to automate this using a tool like cron.

In order to do this, create a renewal script in the home directory of your server user (eg: /home/ec2-user/renew.sh) and add the following:

#!/bin/sh
set -e

cd /home/ec2-user/django-docker-deployment-with-https-using-letsencrypt
/usr/local/bin/docker-compose -f docker-compose.deploy.yml run --rm certbot certbot renew

Then, run chmod +x renew.sh to make it executable. Then run:

crontab -e

Then add the following:

0 0 * * 6 sh /home/ec2-user/renew.sh

This will run the renewal weekly at midnight.

That concludes this guide on how to deploy Django using HTTPS!

18 replies
  1. emre
    emre says:

    Thank you very much for your tutorials, it helps me a lot. I am getting an error related to this topic, but I could not find the problem and I do not understand where I should check.
    nc: bad address ‘proxy’

    Reply
    • Simon
      Simon says:

      Hi Emre, I also had this error, I ‘think’ it’s when nginx configuration in wrong, and in my case that stopped port 80 initialising in certify-init.sh.

      This came down to two things , id put the nginx config files in the proxy rather than the proxy/nginx directory.

      Also I used a sub-domain on an existing dns (not the aws route 53) and I had a small issue with the configuration of that. Hope this helps and not too late for you.

      Reply
      • Derek
        Derek says:

        Hey Simon, I am running into this same issue. Were you able to use your DNS records for your existing DNS or did you end up setting up a new dns record in route 53? I have the same nc: bad address ‘proxy’ error and I would prefer to not use route 53 if possible.

        Reply
    • Muhammed ÖZKAN
      Muhammed ÖZKAN says:

      Selam Emre hatanı çözebildin mi? Aynı hatayı ben de alıyorum paylaşırsan sevinirim.

      Hi Emre, did you solve your error? I’m getting the same error, I would appreciate if you share.

      Reply
  2. Simon
    Simon says:

    Mark,

    Thanks for this, came at just the right time for me as I have an existing Django app I need to containerize ( is that a word ?).

    Coded along and cleared a few self inflicted bugs, tbh the debugging probably helped embed the knowledge all the more.

    Cheers Simon

    Reply
  3. Uzair
    Uzair says:

    proxy_1 | Checking for dhparams.pem
    proxy_1 | Checking for fullchain.pem
    proxy_1 | SSL cert exists, enabling HTTPS…
    proxy_1 | 2022/12/01 22:31:45 [emerg] 8#8: PEM_read_bio_DHparams(“/vol/proxy/ssl-dhparams.pem”) failed (SSL: error:0909006C:PEM routines:get_name:no start line:Expecting: DH PARAMETERS)
    proxy_1 | nginx: [emerg] PEM_read_bio_DHparams(“/vol/proxy/ssl-dhparams.pem”) failed (SSL: error:0909006C:PEM routines:get_name:no start line:Expecting: DH PARAMETERS)

    Reply
  4. none
    none says:

    “Now we can create docker/nginx/Dockerfile and add the following:”
    the path should be
    “docker/proxy/Dockerfile”

    Reply
  5. Samiul Ehsan
    Samiul Ehsan says:

    Idk why I am having one specific problem, in the very last step.. I can retrieve the certificate on running the required command for getting it the first time..
    But then I do “docker-compose … down”, “docker-compose … up” and I get an error saying “…ssl/dhparams.pem doesn’t exist”.. I inspected the volume and checked that the dhparams.pem file exists on the local machine.. What could I be missing out here?

    Reply
  6. jack flavell
    jack flavell says:

    I get an error just after receiving my certificate, basically I have no vol folder?

    So no vol/www for my certificate to go in.

    I’ve also tried creating those folders manually but no dice, that doesnt seem to help

    Does anyone have any ideas at all? Please?

    Reply
  7. uzair
    uzair says:

    Hi,
    I am using this guide to configure my django project on production. I suspect there is ‘static_root’ missing in your setting.py file. I have tried every method to fix static issue but after collectastatic command I have no errors but static files are not loading. I cannot access admin or swagger due to this issue. I would really appreciate if you can help me with this.

    Your work is really helpful.
    UZAIR

    Reply
  8. Bono
    Bono says:

    Hi Mrak, really great tutorial. I just want to ask if i want to deploy it on my local machine, can I used DOMAIN=localhost ?

    Reply
    • mark
      mark says:

      Yes, however you wouldn’t be able to procure a domain certificate for localhost. It has to be a real publicly accessible domain on the internet.

      Reply

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *