How to handle https with docker-compose and mkcert for local development

Published on

May 21, 2021

Docker and docker-compose are now widely used in the world of web development. Since our first Hackathon in 2015, Docker became a standard on all projects:
It helps a lot to maintain our stacks and having specific configurations for local , staging or production environments.

Nevertheless, Antoine has seen a lot of Docker configurations for development environment that are quite far from the staging or production environments. That's why he wrote this article in order to help you understanding, why you need your different environments to be close enough to each other. It's the best way to fail fast on your local environment in order to avoid shipping bugs on production.

Most of the time, local Docker configuration provides access to services directly on a specific port. The holy grail is using a reverse-proxy (such as Traefik) even on development. But today I would like to focus on a very specific topic: the TLS encryption on local environment.

Prerequisites

This article was created using these versions of Docker and mkcert

  • docker: 20.10.15
  • docker-compose: 1.28.5
  • mkcert: 1.4.3-1

The following configuration might work for lower versions but I didn't check.

Traefik

Before we start, we need to talk a bit about Traefik in case you never heard about it.

Basically, Traefik is a reverse proxy, which means it is the door to your platform.
It intercepts and routes every incoming request: it knows all the logic and every rule that determines which services handle which requests
(based on the path, the host, headers, and so on ...).

traefik-concepts-1.png

The cool thing with Traefik is that you don't have much configuration to write.
With only few docker-compose labels on services, you have a powerful reverse-proxy that handles the traffic of your stack.

mkcert

mkcert is a tool to generate locally-trusted development certificates that can be shared with Traefik so it can perform a TLS encryption.

Mkcert can only be used in local environment. Public domain will require you to provide a certificate that is trusted by an external authority.

You can follow the installation guide on the mkcert documentation.

Getting started

We will use the containous/whoami Docker image to demonstrate the usage of a TLS certificate on a docker-compose service.

mkcert certificates

First we have to create and store the TLS certificates with mkcert in the ./certs folder.
These certificates are locally-trusted which mean they are valid only on your machine. Therefore you should gitignore them!
Every developer needs to generate their own certificate when they build the project.

mkdir -p certs
mkcert -cert-file certs/local-cert.pem -key-file certs/local-key.pem "app.localhost" "*.app.localhost" "domain.local" "*.domain.local"

Traefik configuration

Then create the folders: ./docker/traefik

mkdir -p docker/traefik

Create the ./docker/traefik/dynamic_conf.yaml that allows to configure the available subdomains served by Traefik.
It also declares the path of the TLS certifcates (generated with mkcert) that will be shared with Traefik through a Docker volume.

http:
  routers:
    traefik:
      rule: "Host(`traefik.app.localhost`)"
      service: "api@internal"
      tls:
        domains:
          - main: "app.localhost"
            sans:
              - "*.app.localhost"
          - main: "domain.local"
            sans:
              - "*.domain.local"

tls:
  certificates:
    - certFile: "/etc/certs/local-cert.pem"
      keyFile: "/etc/certs/local-key.pem"

Then you have to create the ./docker/traefik/traefik.yaml configuration file.

global:
  sendAnonymousUsage: false

api:
  dashboard: true
  insecure: true

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    watch: true
    exposedbydefault: false

  file:
    filename: /etc/traefik/dynamic_conf.yaml
    watch: true

log:
  level: DEBUG
  format: common

entryPoints:
  web:
    address: ':80'
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

  websecure:
    address: ':443'

I am not going to explain every section of this file but here are some details about the providers and entryPoints sections:

providers

This section tells Traefik what and how services are accessible from the outside. It is basically the routing configuration.

Here we have 2 different providers:

  • docker: Use containers labels to retrieve routing configuration (more information on the next section).
  • file: Load a configuration file. Here the dynamic_conf.yaml we created earlier.

entryPoints

This section is quite simple and allows to redirect every incoming request on http (port 80) to https (port 443).
Now you can be sure that Traefik will not let http requests go further to your services.

docker-compose file

In the docker-compose.dev.yml file, put:

version: '3.8'

services:
  reverse-proxy:
    image: traefik:v2.4.7
    container_name: traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.traefik=true"
      - "traefik.http.routers.traefik.tls=true"
      - "traefik.http.services.traefik.loadbalancer.server.port=8080"
    ports:
      - 80:80
      - 443:443
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    volumes:
      - ./docker/traefik/dynamic_conf.yaml:/etc/traefik/dynamic_conf.yaml:ro
      - ./docker/traefik/traefik.yaml:/etc/traefik/traefik.yaml:ro
      - ./certs:/etc/certs:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro

  whoami:
    image: containous/whoami:latest
    container_name: whoami
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.app.localhost`)"
      - "traefik.http.routers.whoami.tls=true"
    restart: unless-stopped

networks:
  proxy:
    external: true

Here you will find the declarations of services in your stack. Here are some more details about this configuration:

reverse-proxy

  • ports: Binds both ports 80 and 443 to the host. Notice that the whoami service does not expose any port. All incoming requests to your stack will be handled by Traefik.
  • volumes: We share configuration files, certificates and the Docker socket (Traefik requires access to the Docker socket to get its dynamic configuration).

whoami

We define 3 labels:

  • traefik.enable=true: Explicitly tells Traefik to expose this container.
  • traefik.http.routers.whoami.rule=Host('whoami.app.localhost'): The domain the service will respond to.
  • traefik.http.routers.whoami.tls=true: Enable HTTPS on this route.

Run the stack

docker-compose -f docker-compose.dev.yml up -p

And voilà! You should now be able to go to https://whoami.app.localhost.

Any questions or feedback? Ping us on Twitter ! https://twitter.com/KNPLabs

Written by

Antoine Lelaisant
Antoine Lelaisant

Caen

Front and backend developer with a pref for mobile apps. Loves to share his XP with clients & KNPeers during our trainings.

Francois Pasquier
Francois Pasquier

Comments