Traefik 3 and FREE Wildcard Certificates with Docker
In today’s Traefik tutorial we’ll get FREE Wildcard certificates to use in our HomeLab and with all of our internal self-hosted services. We’re going to set up Traefik 3 in Docker and get Let’s Encrypt certificates using Cloudflare as our DNS Provider (we’ll cover how to set up others too). Then we’ll configure local DNS using PiHole (or any other local DNS) to route to our services that are now protected with secure certificates!
Disclosures
- Video was sponsored by UptimeRobot
- Save 20% on UptimeRobot https://l.technotim.live/uptime-robot-technotim!
Looking to do this same thing in Kubernetes? Check out traefik + cert-manager on Kubernetes
Looking for the Traefik + Portainer guide? Check out traefik + portainer on Docker
For reference, the following folder structure was used:
1
2
3
4
5
6
7
./traefik
├── data
│ ├── acme.json
│ ├── config.yml
│ └── traefik.yml
└── cf_api_token.txt
└── docker-compose.yml
You can also do this with other DNS providers too!
Docker Setup
See this post on how to install docker
and docker-compose
Prepare Our Server
Create folder for your compose and mounts
1
2
mkdir docker_volumes
cd docker_volumes
then we’ll create a folder to hold traefik files
1
2
mkdir traefik
cd traefik
create docker compose file and add contents
1
2
touch docker-compose.yaml
nano docker-compose.yaml
Tasks from Our Compose File
Docker Compose Contents
docker-compose.yaml
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
version: "3.8"
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- proxy
ports:
- 80:80
- 443:443
# - 443:443/tcp # Uncomment if you want HTTP3
# - 443:443/udp # Uncomment if you want HTTP3
environment:
CF_DNS_API_TOKEN_FILE: /run/secrets/cf_api_token # note using _FILE for docker secrets
# CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN} # if using .env
TRAEFIK_DASHBOARD_CREDENTIALS: ${TRAEFIK_DASHBOARD_CREDENTIALS}
secrets:
- cf_api_token
env_file: .env # use .env
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data/traefik.yml:/traefik.yml:ro
- ./data/acme.json:/acme.json
# - ./data/config.yml:/config.yml:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=http"
- "traefik.http.routers.traefik.rule=Host(`traefik-dashboard.local.example.com`)"
- "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_CREDENTIALS}"
- "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
- "traefik.http.routers.traefik-secure.entrypoints=https"
- "traefik.http.routers.traefik-secure.rule=Host(`traefik-dashboard.local.example.com`)"
- "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.traefik-secure.tls.domains[0].main=local.example.com"
- "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.local.example.com"
- "traefik.http.routers.traefik-secure.service=api@internal"
secrets:
cf_api_token:
file: ./cf_api_token.txt
networks:
proxy:
external: true
data folder
1
2
3
4
mkdir data
cd data
touch acme.json
chmod 600 acme.json
Traefik Config
1
2
touch traefik.yml
nano traefik.yml
traefik.yml
contents:
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
api:
dashboard: true
debug: true
entryPoints:
http:
address: ":80"
http:
redirections:
entryPoint:
to: https
scheme: https
https:
address: ":443"
serversTransport:
insecureSkipVerify: true
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
# file:
# filename: /config.yml
certificatesResolvers:
cloudflare:
acme:
email: [email protected]
storage: acme.json
# caServer: https://acme-v02.api.letsencrypt.org/directory # prod (default)
caServer: https://acme-staging-v02.api.letsencrypt.org/directory # staging
dnsChallenge:
provider: cloudflare
#disablePropagationCheck: true # uncomment this if you have issues pulling certificates through cloudflare, By setting this flag to true disables the need to wait for the propagation of the TXT record to all authoritative name servers.
#delayBeforeCheck: 60s # uncomment along with disablePropagationCheck if needed to ensure the TXT record is ready before verification is attempted
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
Create Docker Network
1
docker network create proxy
Cloudflare API Token Secret
1
2
touch cf_api_token.txt
nano cf_api_token.txt
Paste your token into file from Cloudflare
Traefik Dashboard Password & .env
make sure you have htpasswd
installed.
To install on Linux
1
2
sudo apt update
sudo apt install apache2-utils
Mac OS (should already be installed)
Windows
htpasswd.exe
Should already be installed on Windows
Generate credential pair
1
echo $(htpasswd -nB user) | sed -e s/\\$/\\$\\$/g
1
2
touch .env
nano .env
paste your credential pair:
e.g.
1
TRAEFIK_DASHBOARD_CREDENTIALS=user:$$2y$$05$$lSaEi.G.aIygyXRdiFpt7OqmUMW9QUG5I1N.j0bXoXxIjxQmoGOWu
Start the stack
1
docker compose up -d --force-recreate
Troubleshooting
Common ways to troubleshoot
1
2
3
docker ps
docker logs traefik
docker exec -it traefik /bin/sh
inside of container
1
2
3
4
5
6
7
8
top
ls
cat acme.json
cat traefik.yml
ls /run/secrets
cat /run/secrets/cf_api_token
echo ${CF_DNS_API_TOKEN_FILE}
echo ${TRAEFIK_DASHBOARD_CREDENTIALS}
DNS
1
nslookup traefik-dashboard.local.example.com
Switch to Production Acme Endpoints
1
2
3
4
...
caServer: https://acme-v02.api.letsencrypt.org/directory # prod (default)
#caServer: https://acme-staging-v02.api.letsencrypt.org/directory # staging
...
Clear out the existing staging certificates
1
2
cd data
nano acme.json
Clear and save
Restart the stack
1
docker compose up -d --force-recreate
Adding Another Workload (NGINX Demo)
1
2
3
4
mkdir nginx
cd nginx
touch docker-compose.yaml
nano docker-compose.yaml
Contents of docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: '3.8'
services:
nginx:
image: nginxdemos/nginx-hello
labels:
- "traefik.enable=true"
- "traefik.http.routers.nginx.rule=Host(`nginx.local.example.com`)"
- "traefik.http.routers.nginx.entrypoints=https"
- "traefik.http.routers.nginx.tls=true"
- "traefik.http.services.nginx.loadbalancer.server.port=8080"
networks:
- proxy
networks:
proxy:
external: true
Check DNS
1
nslookup nginx.local.example.com
Start the new NGINX Stack
1
docker compose up -d --force-recreate
Adding External Routes
Uncomment a few things:
In docker-compose.yaml
1
2
3
...
- ./data/config.yml:/config.yml:ro
...
in traefik.yml
1
2
3
4
...
file:
filename: /config.yml
...
Create config
1
2
3
cd data
touch config.yml
nano config.yml
Contents of config.yml
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
http:
#region routers
routers:
proxmox:
entryPoints:
- "https"
rule: "Host(`proxmox.local.example.com`)"
middlewares:
- default-headers
- https-redirectscheme
tls: {}
service: proxmox
pihole:
#endregion
#region services
services:
proxmox:
loadBalancer:
servers:
- url: "https://192.168.0.17:8006"
passHostHeader: true
#endregion
middlewares:
https-redirectscheme:
redirectScheme:
scheme: https
permanent: true
default-headers:
headers:
frameDeny: true
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
customFrameOptionsValue: SAMEORIGIN
customRequestHeaders:
X-Forwarded-Proto: https
default-whitelist:
ipAllowList:
sourceRange:
- "10.0.0.0/8"
- "192.168.0.0/16"
- "172.16.0.0/12"
secured:
chain:
middlewares:
- default-whitelist
- default-headers
Restart the stack
1
docker compose up -d --force-recreate
To see more examples of commonly used services check out config.yml in the reference files
Final Production Files
Traefik docker-compose.yaml
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
version: "3.8"
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- proxy
ports:
- 80:80
- 443:443
# - 443:443/tcp # Uncomment if you want HTTP3
# - 443:443/udp # Uncomment if you want HTTP3
environment:
CF_DNS_API_TOKEN_FILE: /run/secrets/cf_api_token # note using _FILE for docker secrets
# CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN} # if using .env
TRAEFIK_DASHBOARD_CREDENTIALS: ${TRAEFIK_DASHBOARD_CREDENTIALS}
secrets:
- cf_api_token
env_file: .env # use .env
volumes:
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./data/traefik.yml:/traefik.yml:ro
- ./data/acme.json:/acme.json
- ./data/config.yml:/config.yml:ro
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=http"
- "traefik.http.routers.traefik.rule=Host(`traefik-dashboard.local.example.com`)"
- "traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_DASHBOARD_CREDENTIALS}"
- "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
- "traefik.http.routers.traefik-secure.entrypoints=https"
- "traefik.http.routers.traefik-secure.rule=Host(`traefik-dashboard.local.example.com`)"
- "traefik.http.routers.traefik-secure.middlewares=traefik-auth"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.traefik-secure.tls.domains[0].main=local.example.com"
- "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.local.example.com"
- "traefik.http.routers.traefik-secure.service=api@internal"
secrets:
cf_api_token:
file: ./cf_api_token.txt
networks:
proxy:
external: true
traefik.yml
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
api:
dashboard: true
debug: true
entryPoints:
http:
address: ":80"
http:
redirections:
entryPoint:
to: https
scheme: https
https:
address: ":443"
serversTransport:
insecureSkipVerify: true
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
filename: /config.yml
certificatesResolvers:
cloudflare:
acme:
email: [email protected]
storage: acme.json
caServer: https://acme-v02.api.letsencrypt.org/directory # prod (default)
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory # staging
dnsChallenge:
provider: cloudflare
#disablePropagationCheck: true # uncomment this if you have issues pulling certificates through cloudflare, By setting this flag to true disables the need to wait for the propagation of the TXT record to all authoritative name servers.
#delayBeforeCheck: 60s # uncomment along with disablePropagationCheck if needed to ensure the TXT record is ready before verification is attempted
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
config.yml
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
http:
#region routers
routers:
proxmox:
entryPoints:
- "https"
rule: "Host(`proxmox.local.example.com`)"
middlewares:
- default-headers
- https-redirectscheme
tls: {}
service: proxmox
pihole:
#endregion
#region services
services:
proxmox:
loadBalancer:
servers:
- url: "https://192.168.0.17:8006"
passHostHeader: true
#endregion
middlewares:
https-redirectscheme:
redirectScheme:
scheme: https
permanent: true
default-headers:
headers:
frameDeny: true
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
customFrameOptionsValue: SAMEORIGIN
customRequestHeaders:
X-Forwarded-Proto: https
default-whitelist:
ipAllowList:
sourceRange:
- "10.0.0.0/8"
- "192.168.0.0/16"
- "172.16.0.0/12"
secured:
chain:
middlewares:
- default-whitelist
- default-headers
Join the conversation
Traefik 3 is here! So, today we'll get trusted certificates with Let's Encrypt for all of our self-hosted services! No more https warnings and no more weird ports!https://t.co/MoRKYXvA0M pic.twitter.com/5OR22iRJRJ
— Techno Tim (@TechnoTimLive) April 30, 2024
Links
🛍️ Check out the new Merch Shop at https://l.technotim.live/shop
⚙️ See all the hardware I recommend at https://l.technotim.live/gear
🚀 Don’t forget to check out the 🚀Launchpad repo with all of the quick start source files
🤝 Support me and help keep this site ad-free!