Local HTTPS development with Docker compose, Traefik and Mkcert.

A lock on a blue door
A lock on a blue door
Photo by Erik Mclean on Unsplash

Why using HTTPS in development?

The need to use HTTPS in development may seem anecdotal, however, several cases meet this need:

  • To perform certain tests locally (such as lighthouse audits)
  • Avoid CORS errors thanks to reverse proxies
  • To respond to point number 10 of the 12 factor app which suggests that the production and dev environments should be as similar as possible.

How to implement it using docker-compose?

To explain how to implement an HTTPS environment in dev, I will use a small project that will serve as an example throughout this article.

This project is based on 2 microservices:

  • An “API” written in Flask / Python
  • A whoami container (it’s an image based on Go who return info about the host when you request it)

The reverse-proxy I will use for this project is Traefik as described in docs:

Traefik is an open-source Edge Router that makes publishing your services a fun and easy experience. It receives requests on behalf of your system and finds out which components are responsible for handling them.

It’s easy to use and work well with docker-compose.

Finally, the last utility we will use, and it’s the one who will allow us to use HTTPS locally is mkcert. It allows us to create TLS certificates who are locally trusted by a Certificate Authority installed and managed by mkcert itself.

This is what our project tree looks like:

.
|____docker-compose.yml
|____traefik.config.toml
|____traefik.toml
|____certs
| |____foo.bar.pem
| |____foo.bar-key.pem
|____api
| |____requirements.txt
| |____Dockerfile
| |____main.py

Nothing much to add about it, the main configs file are at the root of the project and the Flask API files got their folder.

To describe how this works we will review each service from the :

version: '3'services:
back-end:
build:
context: api
dockerfile: Dockerfile
whoami:
image: containous/whoami
ports:
- 5001:80
traefik:
image: traefik:latest
restart: always
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- $PWD/certs:/etc/certs
- $PWD/traefik.toml:/etc/traefik/traefik.toml
- $PWD/traefik.config.toml:/etc/traefik/traefik.config.toml

The docker-compose file describes 3 services:

  • The back-end (Flask application)
  • whoami (The Go based image to return info about the host)
  • traefik (the http/tcp/udp reverse-proxy)

The first service we will review is our extremely complex Flask API. The latter is single-file and consists of a single endpoint that returns “Hello, world” (told you)

from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)

Nothing much to add, we just have an app running on localhost on port 5000

click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
Werkzeug==1.0.1

FROM python:latest
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
ADD . /app
EXPOSE 5000
CMD ["python", "main.py"]

This dockerfile is not suited for production because it uses the development server Flask is offering us. For a production grade image, you might want to use an image based on a wsgi server like gunicorn. However, this image is still useful during development, because it allows us to have hot reload of any changes made in our code.

The second service we’ll use is a Whoami docker image, there’s no code to write here, we’ll just have to run a container based on this image.

The third service that is used in our docker-composer is Traefik. The latter will require two configuration files, a “static” config file which will be the startup file of the application and a “dynamic” config file which will define our routes policies.

[global]
sendAnonymousUsage = false
[log]
level = "INFO" # Change to "DEBUG" if you need more informations
format = "common"
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.https]
address = ":443"
[providers]
[providers.file]
filename = "/etc/traefik/traefik.config.toml"
watch = true
[api]
insecure = true
dashboard = true

[http]
[http.routers]
# Here we declare the HTTP/HTTPS route to our Flask API, HTTP
# requests will be redirected to HTTPS by using the redirect
# middleware
[http.routers.http-to-back-end]
rule = "Host(`foo.bar`)"
service = "back-end"
entrypoints = ["http"]
middlewares = ["redirect"]
[http.routers.https-to-back-end]
rule = "Host(`foo.bar`)"
service = "back-end"
entrypoints = ["https"]
[http.routers.https-to-back-end.tls]
# Here we define another rule for the whoami container
# it needs the /whoami path
# Like the back-end route, it redirect http to https
[http.routers.http-to-whoami]
rule = "Host(`foo.bar`) && Path(`/whoami`)"
service = "whoami"
entrypoints = ["http"]
middlewares = ["redirect"]
[http.routers.https-to-whoami]
rule = "Host(`foo.bar`) && Path(`/whoami`)"
service = "whoami"
entrypoints = ["https"]
[http.routers.https-to-whoami.tls]
# We define our services here, both of our service are loadBalancer
# who will redirect to our docker container (back-end or whoami
# based on router rules)
[http.services]
[http.services.back-end]
[http.services.back-end.loadBalancer]
passHostHeader = true
[[http.services.back-end.loadBalancer.servers]]
url = "http://back-end:5000/"
[http.services.whoami]
[http.services.whoami.loadBalancer]
passHostHeader = true
[[http.services.whoami.loadBalancer.servers]]
url = "http://whoami:80/"
[http.middlewares]
[http.middlewares.redirect.redirectScheme]
scheme = "https"
# The path in the container to the tls certificates generated by
# mkcert
[tls]
[[tls.certificates]]
certFile = "/etc/certs/foo.bar.pem"
keyFile = "/etc/certs/foo.bar-key.pem"%

Now that we got everything defined, we need one last element to make the stack work with HTTPS locally.

In the Traefik configuration, we defined some TLS certificates to use, to generate those we will simply just use mkcert.

It’s straightforward to install mkcert on your system:

  • On macOS:
brew install mkcert
brew install nss # if you use Firefox
  • For Linux & Windows installation, there are some lines in the documentation who cover your OS:

Once mkcert is installed on your computer, there are 3 steps’ to follow:

Install the local CA:

mkcert -install # Install the local CA in the system trust store

Generate your certificates:

mkcert foo.bar # Generate foo.bar.pem and foo.bar-key.pen

Modify your /etc/hosts by adding this line at the end of the file:

127.0.0.1 foo.bar

Once all those steps are done, you can run your docker-compose stack:

docker-compose up
A terminal with stdout from docker-compose showing the 3 services running.
A terminal with stdout from docker-compose showing the 3 services running.
Hurrah !

Now you should be able to access the Flask API from your web browser using https://foo.bar:

A picture showing a web browser with foo.bar address, also show a secured connexion label.
A picture showing a web browser with foo.bar address, also show a secured connexion label.
The certificat should be emited by mkcert if you click on it.

Now if you access https://foo.bar/whoami you should get some info about your whoami container like this:

Hostname: 821c712dd6ce
IP: 127.0.0.1
IP: 172.23.0.3
RemoteAddr: 172.23.0.2:51872
GET /whoami HTTP/1.1
Host: foo.bar
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6)

And if you access whoami directly with localhost:5001:

Hostname: 821c712dd6ce
IP: 127.0.0.1
IP: 172.23.0.3
RemoteAddr: 172.23.0.1:43186
GET / HTTP/1.1
Host: localhost:5001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6)

Hostnames are the same, it’s the container id you can get by typing:

docker ps

However, the hosts are different and correspond to either your local TLS certificates domain name or simply your localhost.

To conclude:

This method give you the ability to work in an environment who is closer to your production environment will give you a way to work on your code without building/deploying to a staging/production area every time to verify some implementation where you might need HTTPS.

I’m good at Googling and browsing Stack Overflow

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store