Tip #17: my self-host docker setup with ssl certs
Intro
I run a few docker images at home which are local only. Some examples:
The TL;DR is:
Each of these services has a DNS name and present a fully trusted SSL certificate. I use Traefik as reverse proxy which also does the DNS challenge with Cloudflare for the Let’s Encrypt SSL certificates. There is a DNS A record and a wildcard DNS A record that points to my local IP of the docker server for a subdomain called local, configured in Cloudflare.
This is a nice setup that is easy to maintain and that works well for me. Let me share how that’s done.
One-time setup
The DNS and reverse Proxy (Traefik) setup needs to be done only once. For that I assume:
- you have a domain with Cloudflare and know how to get the API key
- you have a server with docker installed
DNS
A subdomain for my public domain, called local, is used for my DNS resolution locally. There is a wildcard * A record that points to my Docker server local IP Address.
Assuming 192.168.1.2
is the docker server local IP address, the DNS records look like this in Cloudflare:
Type | Name | Content | Proxy Status |
---|---|---|---|
A | *.local | 192.168.1.2 | DNS only - reserved IP |
A | local | 192.168.1.2 | DNS only - reserved IP |
This means that local.yourdomain.com
or anything.local.yourdomain.com
will point to 192.168.1.2
.
File Folder Structure
I have a folder called docker-compose in my home directory. Each service has its own folder with a docker-compose.yml
file and a .env
file. The .env
file contains the environment variables that are used in the docker-compose.yml
file.
Like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/home
└── docker-compose
├── mailrise
│ ├── docker-compose.yml
│ └── .env
├── portainer
│ ├── docker-compose.yml
│ └── .env
├── traefik
│ ├── docker-compose.yml
│ └── .env
└── your_spotify
├── docker-compose.yml
└── .env
Reverse proxy with Traefik
I use Traefik to redirect traffic to the right docker container and to manage SSL certificates.
Traefik settings:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
version: "3.9"
networks:
public:
services:
traefik:
image: "traefik:latest"
container_name: "traefik"
restart: always
command:
# Globals / Dashboard
#- "--log.level=DEBUG"
- "--api=true"
- "--api.dashboard=true"
- "--global.sendAnonymousUsage=false"
# Docker
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.endpoint=unix:///var/run/docker.sock"
# Entrypoints
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# LetsEncrypt
- "--certificatesresolvers.mydnschallenge.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.mydnschallenge.acme.dnschallenge.provider=cloudflare"
- "--certificatesresolvers.mydnschallenge.acme.storage=acme/acme.json"
- "--certificatesresolvers.mydnschallenge.acme.dnschallenge.delaybeforecheck=0"
- "--certificatesresolvers.mydnschallenge.acme.dnschallenge.resolvers=1.1.1.1:53,1.0.0.1:53"
# !IMPORTANT - COMMENT OUT THE FOLLOWING LINE IN PRODUCTION!
- "--certificatesresolvers.mydnschallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
ports:
- "80:80"
- "443:443"
environment:
- CLOUDFLARE_EMAIL=${CF_API_EMAIL}
- CLOUDFLARE_API_KEY=${CF_API_KEY}
volumes:
- "acme-data:/acme"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
labels:
# API
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`${HOST_NAME}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.entrypoints=web,websecure"
# Wildcard cert
- "traefik.http.routers.traefik.tls.domains[0].main=${DOMAIN}"
- "traefik.http.routers.traefik.tls.domains[0].sans=*.${DOMAIN}"
- "traefik.http.routers.traefik.tls.certresolver=mydnschallenge"
# Global http-->https
- "traefik.http.routers.http-catchall.rule=hostregexp(`{host:[a-z-.]+}`)"
- "traefik.http.routers.http-catchall.entrypoints=web"
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
# BasicAuth Middleware
- "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_USER}:${TRAEFIK_PASSWORD}"
- "traefik.http.routers.traefik.middlewares=auth"
networks:
- public
volumes:
acme-data:
name: acme-data
Test with the staging server first, then comment out the line:
- "--certificatesresolvers.mydnschallenge.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
to use the production server instead of the staging server
The environment variables as follows:
1
2
3
4
5
6
7
DOMAIN=yourdomain.com
HOST_NAME=local.yourdomain.com
CF_API_KEY=redacted
CF_API_EMAIL=redacted
LETSENCRYPT_EMAIL=redacted
TRAEFIK_USER=redacted
TRAEFIK_PASSWORD=redacted
To start Traefik, run:
1
docker-compose --env-file .env up
Essentially it will:
- expose the Traefik dashboard at
local.yourdomain.com/dashboard
, protected by basic auth (TRAEFIK_USER:TRAEFIK_PASSWORD) - redirect all http: traffic to https:
- SSL certificates will be generated by Let’s Encrypt
- wildcard SSL certificate for
yourdomain.com
and*.yourdomain.com
(not sure if this is necessary) - Cloudflare as a DNS challenge provider
- wildcard SSL certificate for
Per docker image setup
To streamline the process of creating new Docker images with reverse proxy and SSL certificate, I made a template:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: "3.9"
networks:
traefik_public:
name: traefik_public
services:
web:
image: ${SERVICE_NAME}:latest
restart: always
container_name: ${SERVICE_NAME}
labels:
- "traefik.enable=true"
- "traefik.http.routers.${SERVICE_NAME}.rule=Host(`${SERVICE_NAME}.${SUBDOMAIN}`)"
- "traefik.http.routers.${SERVICE_NAME}.entrypoints=websecure"
- "traefik.http.routers.${SERVICE_NAME}.tls.certresolver=mydnschallenge"
networks:
- traefik_public
The environment variables as follows:
1
2
SERVICE_NAME=yourservicename
SUBDOMAIN=local.yourdomain.com
The SERVICE_NAME
will be used as the container image, container name and the host name of the SUBDOMAIN
that will be proxied to https:.
To start the service, run:
1
docker-compose --env-file .env up
It will:
- create a docker container with the yourservicename
- generate an SSL certificates by Let’s Encrypt
- Serve the exposed port of the docker image at
https://yourservicename.local.yourdomain.com
Abstract
I have documented this setup for my own reference, but I hope it will be useful for others as well. If you have any questions, feel free to post a comment.