My Traefik v3 configuration for homelab, without docker provider, only file-based templated dynamic configuration, static configuration and docker compose. My use case is a single domain and multiple subdomains, and this configuration generated a single wildcard certificate for all of them.
/root/traefik
├── .env
├── compose.yml
├── services
│ ├── common
│ │ ├── headers.yml
│ │ └── middlewares.yml
│ ├── photos.yml
│ ├── server.yml
│ └── vault.yml
├── template.yml
└── traefik.yml
Environment variables
# Primary domain
DOMAIN=potat.dev
# Email for LE expiration notices
ACME_EMAIL=user@example.com
# Cloudflare API token
CF_DNS_API_TOKEN=cfut_abc123
# Timeouts for strict ISP
# CF_HTTP_TIMEOUT=120
# CF_POLLING_INTERVAL=10
# CF_PROPAGATION_TIMEOUT=300Token must have the following permissions:
- Zone / Zone / Read
- Zone / DNS / Edit
If you are getting failed to create TXT record or context deadline exceeded error - uncomment timeouts section
Source: https://go-acme.github.io/lego/dns/cloudflare
Docker Compose configuration
services:
traefik:
image: traefik:v3.7
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
env_file:
- .env
command:
- "--entryPoints.websecure.http.tls.domains[0].main=${DOMAIN}"
- "--entryPoints.websecure.http.tls.domains[0].sans=*.${DOMAIN}"
ports:
- "80:80"
- "443:443"
volumes:
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./services:/etc/traefik/services:ro
- certs:/certs
volumes:
certs:
name: traefik_certsDomain settings are configured here because static configuration does not support environment variable substitution, whereas this method does. Static configuration is being merged together from the traefik.yml file and these parameters
If you need an additional domain, add DOMAIN_SECONDARY to the .env file and add two more lines using domains[1]
Warning
Traefik v2 allowed you to write entryPoints as entrypoints, but v3 no longer does
Static configuration, also known as Install configuration
global:
checkNewVersion: true
sendAnonymousUsage: false
api:
dashboard: true
insecure: false
log:
level: INFO
entryPoints:
web:
address: ":80"
asDefault: false
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
asDefault: true
http:
tls:
certResolver: cloudflare
providers:
file:
directory: "/etc/traefik/services"
watch: true
certificatesResolvers:
cloudflare:
acme:
storage: "/certs/acme.json"
dnsChallenge:
provider: cloudflare
propagation:
# disablePropagationCheck: true
delayBeforeChecks: 10s
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
- "1.0.0.1:53"
- "8.8.4.4:53"
serversTransport:
insecureSkipVerify: trueinsecureSkipVerifyallows using insecure URLs withhttps://protocol and missing SSL certs (e.q. Proxmox)disablePropagationCheckskips the DNS propagation checks before notifying ACME that the DNS challenge is readywebsecure.asDefaultallows you to avoid specifying an entry point for each route
Service configuration template
{{ $app := "app" }}
{{ $url := "http://10.0.10.XX:80" }}
http:
routers:
{{ $app }}-router:
rule: Host(`{{ $app }}.{{ env "DOMAIN" }}`)
service: "{{ $app }}-service"
middlewares:
- "secure-headers"
# - "{{ $app }}-middleware"
services:
{{ $app }}-service:
loadBalancer:
servers:
- url: "{{ $url }}"
passHostHeader: true
# middlewares:
# {{ $app }}-middleware:
# headers:
# customRequestHeaders:
# X-Custom-Header: "Example"How to use:
- Run
cp template.yml services/service.ymlto deploy new service - Edit
$appand$urlvariables -$appwill be your subdomain - Custom middleware sections can be removed
How this works: Traefik uses Go text/template engine to parse expressions in dynamic configuration
Example configuration for Proxmox
{{ $app := "server" }}
{{ $url := "https://10.0.10.42:8006" }}
http:
routers:
{{ $app }}-router:
rule: Host(`{{ $app }}.{{ env "DOMAIN" }}`)
service: "{{ $app }}-service"
middlewares:
- "secure-headers"
services:
{{ $app }}-service:
loadBalancer:
servers:
- url: "{{ $url }}"
passHostHeader: trueCommon secure headers configuration
http:
middlewares:
secure-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
customFrameOptionsValue: SAMEORIGIN