Skip to content

Instantly share code, notes, and snippets.

@potat-dev
Last active July 6, 2026 09:27
Show Gist options
  • Select an option

  • Save potat-dev/6b3d3db03dc4172ecd03af9e6a3fa4d9 to your computer and use it in GitHub Desktop.

Select an option

Save potat-dev/6b3d3db03dc4172ecd03af9e6a3fa4d9 to your computer and use it in GitHub Desktop.
Simple Traefik Wildcard Templated Setup

Traefik Setup

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.

File structure

/root/traefik
├── .env
├── compose.yml
├── services
│   ├── common
│   │   ├── headers.yml
│   │   └── middlewares.yml
│   ├── photos.yml
│   ├── server.yml
│   └── vault.yml
├── template.yml
└── traefik.yml

.env

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=300

Token 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

compose.yml

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_certs

Domain 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

traefik.yml

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: true
  • insecureSkipVerify allows using insecure URLs with https:// protocol and missing SSL certs (e.q. Proxmox)
  • disablePropagationCheck skips the DNS propagation checks before notifying ACME that the DNS challenge is ready
  • websecure.asDefault allows you to avoid specifying an entry point for each route

template.yml

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.yml to deploy new service
  • Edit $app and $url variables - $app will be your subdomain
  • Custom middleware sections can be removed

How this works: Traefik uses Go text/template engine to parse expressions in dynamic configuration

services/server.yml

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: true

services/common/headers.yml

Common secure headers configuration

http:
  middlewares:
    secure-headers:
      headers:
        stsSeconds: 31536000
        stsIncludeSubdomains: true
        forceSTSHeader: true
        contentTypeNosniff: true
        browserXssFilter: true
        customFrameOptionsValue: SAMEORIGIN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment