This guide walks you through setting up Traefik v3 as a reverse proxy using the file provider for dynamic configuration. We’ll deploy it to serve Plex (via plex.randomdomain.com) and a general website (www.randomdomain.com), with CrowdSec integration for security.

The real neat thing we do in this example is setup a TCP router and service for Plex in addition to the HTTP router and service, along with the additional Middleware to provide what I’ve come to understand to need. This likely could be updated over time, but as of now, this works.

The Dashboard is being provided insecurely, but, we have it running on a different port and so long as you don’t port forward that one too, you can internally safely use it. IMHO – I would never put my api/dashboard on the public internet and would only access it externally via a VPN.

The CrowdSec integration is a logical extra element here, but can be ignored and and the crowdsec elements of the docker-compose.yml be deleted, the ‘crowdsec’ can be left out of the router’s middlewares areas, and the last traefik.yaml CrowdSec file can be ignored.

The external and internal domain use might be new, but usually your intranet uses an internal domain and you external face resolves the external domain, the port forwarding for 80 and 443 on your public IPs to your Traefik container’s IP will allow it to respond to those requests and is otherwise internally configured to handle the paths appropriately.

Neat thing is, we use hostnames from Traefik to its services so those can be internal hostnames (and fqdns like wwwhost.homelab.home) and Traefik is going to work out the reverse proxy across the path. You can setup IPs in services but note that having an internal BIND9 server/local DNS resolution really provides the surface necessary to easily service to host regardless of IPv4 or IPv6.

When all said and done you should have a collection of files:

./docker-compose.yml
./traefik.yml
./dynamic/routers.yml
./dynamic/services.yml
./dynamic/middlware.yml
./acquis.d/traefik.yaml

🔧 Prerequisites

Before starting, ensure:

  1. Your host/vm has at least two NICs to use as macvlan interfaces (see below) in addition to the host’s primary NIC. This allows you do disable DHCP on those interfaces so they will purely be used for Docker Network traffic.
  2. The createdmzmacvlan.sh and createmediamacvlan.sh scripts are adapted for your network (see Macvlan Setup section).

Example Netplan for Docker Network

Netplan: /etc/netplan/50-network-config.yaml

network:
    ethernets:
        eth0:
            dhcp4: true
            dhcp6: true
        eth1:
            dhcp4: false
            dhcp6: false
            optional: true
        eth2:
            dhcp4: false
            dhcp6: false
            optional: true

🛠️ Step 1: Create Macvlan Networks

Run the following scripts to create IPv4 and IPv6 subnets on your host’s secondary NICs (e.g., eth0 or enp6sX). Replace parent=eth0 with your actual interface. The example IPv6 addresses are completely fake and you will have to use one that is available likely as a part of the /56 from your ISP.

Script: createdmzmacvlan.sh

#!/bin/bash
docker network create \
    --driver=macvlan \
    --subnet=192.168.5.0/24 --gateway=192.168.5.1 \
    --ip-range=192.168.5.64/27 \
    --ipv6 --subnet=2a00:xxxx::/64 --gateway=2a00:xxxx::1 \
    --attachable \
    -o parent=eth1 -o macvlan_mode=bridge \
    dockerdmz

Script: createmediamacvlan.sh

#!/bin/bash
docker network create \
    --driver=macvlan \
    --subnet=192.168.1.0/24 --gateway=192.168.1.1 \
    --ip-range=192.168.1.64/27 \
    --ipv6 --subnet=2a00:xxxx::/64 --gateway=2a00:xxxx::1 \
    --attachable \
    -o parent=eth2 -o macvlan_mode=bridge \
    dockermedia

⚠️ Ensure the parent interface and gateway match your host’s network configuration.


📦 Step 2: Deploy Traefik with Docker Compose

Replace all IP addresses, hostnames, and domains in this template that are generic placeholders (e.g., 192.168.0.66, traefikproxy, plex.homelab.home) with the actual IPs/Hostnames/MACs that you intend them to be.

This docker-compose.yml template does assume you have an “.env" file setup with the KEY=VALUE elements required in the “environment:” section. The idea here is that you can safely store sensitive values in the “.env" file and have references that will load that value into the run of the docker-compose.yml and for that to work you might be required to install “python-dotenv“.

docker-compose.yml Template

services:
  traefik:
    container_name: traefikproxy
    image: traefik:latest
    hostname: traefikproxy
    domainname: homelab.home
    expose:
      - "80"
      - "80/udp"
      - "8080"
      - "443"
      - "443/udp"
      - "8880"
      - "8880/udp"
      - "8883"
      - "8883/udp"
    volumes:
      - ./captcha.html:/captcha.html:ro
      - ./ban.html:/ban.html:ro
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - ./dynamic:/etc/traefik/dynamic:ro
      - ./traefik-logs:/var/log/traefik
      - ./.htaccess:/etc/traefik/.htaccess:ro
      - traefik-certificates:/letsencrypt
      - traefik-data:/data
    labels:
      - "traefik.enable=false"
    networks:
      dockerdmz:
        mac_address: 02:42:ac:46:ac:88
        ipv4_address: 192.168.5.66
        ipv6_address: 2a00:xxxx::10
    dns:
    - 192.168.1.65
    - 192.168.0.65
    restart: unless-stopped
    command:
      - "--configFile=/etc/traefik/traefik.yml"
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 5s
  crowdsec:
    container_name: traefikcrowdsec
    image: crowdsecurity/crowdsec:latest
    hostname: traefikcrowdsec
    domainname: homelab.home
    environment:
      COLLECTIONS: crowdsecurity/plex crowdsecurity/magento crowdsecurity/appsec-crs crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules crowdsecurity/appsec-wordpress crowdsecurity/traefik crowdsecurity/whitelist-good-actors crowdsecurity/linux crowdsecurity/base-http-scenarios crowdsecurity/http-cve crowdsecurity/nginx crowdsecurity/wordpress timokoessler/gitlab
      PARSERS: crowdsecurity/whitelists crowdsecurity/pam-logs crowdsecurity/docker-logs crowdsecurity/geoip-enrich
      SCENARIOS: crowdsecurity/http-cve-probing crowdsecurity/http-generic-bf crowdsecurity/http-dos-invalid-http-versions crowdsecurity/http-admin-interface-probing crowdsecurity/http-wordpress_wpconfig ltsich/http-w00tw00t crowdsecurity/http-probing crowdsecurity/http-bf-wordpress_bf_xmlrpc crowdsecurity/http-backdoors-attempts crowdsecurity/http-bf-wordpress_bf crowdsecurity/http-crawl-non_statics crowdsecurity/http-open-proxy crowdsecurity/http-sensitive-files crowdsecurity/http-sqli-probing crowdsecurity/http-wordpress-scan crowdsecurity/http-wordpress_user-enum crowdsecurity/http-xss-probing crowdsecurity/http-bad-user-agent crowdsecurity/http-cve-2021-41773 crowdsecurity/http-cve-2021-42013 crowdsecurity/http-path-traversal-probing aidalinfo/tcpudp-flood-traefik
      POSTOVERFLOWS: 
      CONTEXTS: crowdsecurity/appsec_base crowdsecurity/suricata_base
      APPSEC_CONFIGS: crowdsecurity/appsec-default crowdsecurity/crs crowdsecurity/generic-rules crowdsecurity/virtual-patching
      APPSEC_RULES: 
      DISABLE_LOCAL_API: ${LOCALAPIDISABLE}
      AGENT_USERNAME: ${CROWDSEC_AGENT_USERNAME}
      AGENT_PASSWORD: ${CROWDSEC_AGENT_PASSWORD}
      LOCAL_API_URL: ${CROWDSEC_LOCAL_API_URL}
      TZ: America/Chicago
    expose:
      - "8080"
      - "6060"
      - "7422"
    volumes:
      - ./acquis.d/traefik.yaml:/etc/crowdsec/acquis.d/traefik.yaml:ro
      - /etc/localtime:/etc/localtime:ro
      - ./traefik-logs:/var/log/traefik:ro
    restart: unless-stopped
    labels:
      - "traefik.enable=false"
    networks:
      dockermedia:
        mac_address: 02:42:ac:46:ac:89
        ipv4_address: 192.168.1.68
        ipv6_address: 2a00:xxxx::10
        aliases:
          - traefikcrowdsec.homelab.home
          - traefikcrowdsec
    dns:
    - 192.168.1.65
    - 192.168.0.65
    healthcheck:
      test: ["CMD", "cscli", "version"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

networks:
  dockerdmz:
    external: true
  dockermedia:
    external: true

volumes:
  traefik-data:

📄 Step 3: Configure Traefik with traefik.yml

Ensure the file provider points to your dynamic config directory. Use IPv4/IPv6 in entrypoints, and enable CrowdSec plugin:

traefik.yml

global:
  checkNewVersion: true
  sendAnonymousUsage: false
api:
  dashboard: true
  insecure: true
ping:
  entryPoint: "ping"
log:
  level: ERROR
accessLog:
  filePath: /var/log/traefik/access.log
  format: json
  filters:
    statusCodes:
      - "100-199" # log informational http requests
      - "200-299" # log successful http requests
      - "300-399" # log redirects
      - "400-499" # log failed http requests
      - "500-599" # log server errors
    #retryAttempts: true # where at least one retry was attempted
    minDuration: 0ms # log all requests
  # collect logs as in-memory buffer before writing into log file
  bufferingSize: 0
  fields:
    defaultMode: keep # keep all fields per default
    headers:
      defaultMode: keep
metrics:
  prometheus:
    buckets:
      - 0.1
      - 0.3
      - 1.2
      - 5.0
    addRoutersLabels: true
    entryPoint: metrics
    headerLabels:
      useragent: User-Agent
providers:
  file:
    directory: /etc/traefik/dynamic
    watch: true
experimental:
  plugins:
    bouncer:
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: v1.4.2
entryPoints:
  ping:
    address: :8082
  metrics:
    address: :8084
  web:
    address: :80
    http2:
      maxConcurrentStreams: 250
    transport:
      respondingTimeouts:
        readTimeout: '600'
  websecure:
    address: :443
    http2:
      maxConcurrentStreams: 250
    transport:
      respondingTimeouts:
        readTimeout: '600'
    http3:
      advertisedPort: 443
certificatesResolvers:
  letsEncrypt:
    acme:
      email: adddress@domain.com
      storage: /letsencrypt/acme.json
      tlsChallenge: true

📁 Step 4: Dynamic Configuration Files

Create a dynamic/ directory with the following files. Replace domains like plex.randomdomain.com and www.randomdomain.com as needed.

services.yml

tcp:
  services:
    plexdirecttcptls:
      loadBalancer:
        servers:
          - address: 'plex.homelab.home:32400'
        serversTransport: plexdirecttcp
    plexdirecttcp:
      loadBalancer:
        servers:
          - address: 'plex.homelab.home:32400'
        serversTransport: plexdirecttcp
  serversTransports:
    plexdirecttcp:
      dialTimeout: 30s
      dialKeepAlive: 30s
      terminationDelay: 100ms
http:
  services:
    plexhttps:
      loadBalancer:
        servers:
          - url: "http://plexserver:32400"
        serversTransport: homelabtransport
    plexdirecthttp:
      loadBalancer:
        servers:
          - url: "http://plexserver:32400"
        serversTransport: plexdirecthttp
    wwwservice:
      loadBalancer:
        servers:
          - url: "http://wwwserver:80"
        serversTransport: homelabtransport
  serversTransports:
    homelabtransport:
      maxIdleConnsPerHost: 7
      forwardingTimeouts:
        dialTimeout: "10s"
        responseHeaderTimeout: "5s"
        idleConnTimeout: "15s"
        readIdleTimeout: "2s"
        pingTimeout: "15s"
    plexdirecthttp:
      insecureSkipVerify: true

routers.yml

tcp:
  routers:
    plexdirecttcptls:
      rule: HostSNIRegexp(`\.plex\.direct$`)
      entrypoints:
        - websecure
      service: plexdirecttcptls
      tls:
        passthrough: true
    plexdirecttcp:
      rule: HostSNIRegexp(`\.plex\.direct$`)
      entrypoints:
        - web
      service: plexdirecttcp
http:
  routers:
    plexrandomdomaincom:
      service: plexhttps
      rule: Host(`plex.randomdomain.com`) || HostRegexp(`(?i)^plex\.randomdomain\.com$`)
      middlewares:
        - crowdsec
        - redirect-to-https
        - plex-headers-ssl
      entryPoints:
        - web
        - websecure
      tls:
        certResolver: letsEncrypt
    plexdirecthttp:
      service: plexdirecthttp
      rule: HostRegexp(`\.plex\.direct`)
      middlewares:
        - crowdsec
        - plex-headers
      entryPoints:
        - web
    wwwrandomdomaincom:
      rule: Host(`www.randomdomain.com`) || HostRegexp(`(?i)^www\.randomdomain\.com$`)
      service: wwwservice
      middlewares:
        - crowdsec
      entryPoints:
        - web
        - websecure
      tls:
        certResolver: letsencrypt

middlewares.yml

http:
  middlewares:
    crowdsec:
      plugin:
        bouncer:
          enabled: true
          logLevel: ERROR
          LogFilePath: "/var/log/traefik/crowdsec-bouncer.log"
          updateIntervalSeconds: 60
          updateMaxFailure: 0
          defaultDecisionSeconds: 60
          httpTimeoutSeconds: 10
          crowdsecMode: live
          crowdsecAppsecEnabled: true
          crowdsecAppsecHost: traefikcrowdsec.homelab.home:7422
          crowdsecAppsecPath: "/"
          crowdsecAppsecFailureBlock: true
          crowdsecAppsecUnreachableBlock: true
          crowdsecAppsecBodyLimit: 10485760
          crowdsecLapikey: "CHANGEME"
          crowdsecLapischeme: http
          crowdsecLapihost: crowdsec.homelab.home:8080
          crowdsecLapiPath: "/"
          forwardedHeadersTrustedIPs:
            - 192.168.0.0/16
            - 2a00:xxxx::/64
          clientTrustedIPs: 
            - 192.168.0.0/16
            - 2a00:xxxx::/64
          forwardedHeadersCustomName: X-Forwarded-For
          redisCacheEnabled: false
          redisCacheHost: "traefikcrowdsecredis.homelab.home:6379"
          redisCachePassword: CHANGEME
          redisCacheDatabase: "5"
          redisCacheUnreachableBlock: false
          captchaProvider: recaptcha
          captchaSiteKey: CHANGEME
          captchaSecretKey: CHANGEME
          captchaGracePeriodSeconds: 1800
          captchaHTMLFilePath: /captcha.html
          banHTMLFilePath: /ban.html
    plex-headers-ssl:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
        accessControlMaxAge: 100
        frameDeny: true
        browserXssFilter: true
        contentTypeNosniff: true
        sslRedirect: true
        sslHost: plex.ke0bjp.net
        sslForceHost: true
        stsSeconds: 63072000
        stsIncludeSubdomains: true
        stsPreload: true
        customFrameOptionsValue: allow-from https:plex.randomdomain.com https:ke0bjp.net https:plex.direct
        referrerPolicy: "same-origin"
        featurePolicy: "camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none';"
        customResponseHeaders:
          X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex,"
          server: "" # hide server info from visitors
        contentSecurityPolicy: >
          default-src 'none'; base-uri 'self' plex.randomdomain.com; 
          font-src 'self' data: plex.randomdomain.com; 
          media-src 'self' data: blob: plex.randomdomain.com https://*.plex.direct:32400 https://video.internetvideoarchive.net https://*.cloudfront.net; 
          script-src 'self' 'unsafe-inline' 'unsafe-eval' randomdomain.com plex.ke0bjp.net; 
          style-src 'self' 'unsafe-inline' plex.randomdomain.com; img-src 'self' data: blob: https: plex.randomdomain.com; 
          worker-src * blob:; 
          frame-src 'self'; 
          connect-src 'self' https: randomdomain.com plex.randomdomain.com wss://*.plex.direct:32400 wss://pubsub.plex.tv; object-src 'self' plex.ke0bjp.net; 
          frame-ancestors 'self' randomdomain.com plex.randomdomain.com plex.direct; 
          form-action 'self' plex.randomdomain.com; 
          manifest-src 'self' plex.randomdomain.com; 
          script-src-elem 'self' 'unsafe-inline' randomdomain.com plex.randomdomain.com www.gstatic.com;

🔐 Step 5: Configure CrowdSec for Protection

This acquis.d/traefik.yaml file in CrowdSec’s config points the Agent to the Traefik logs and establishes an Appsec listener that the Traefik Crowdsec Bouncer Plugin will interact with to determine if the request is benign or not. The CrowdSec Appsec and Agent Parser will be interacting with the main CrowdSec LAPI in this example, but, there are other ways to set this up.

Example: crowdsec/acquis.d/traefik.yaml

poll_without_inotify: false
filenames:
  - /var/log/traefik/*.log
labels:
  type: traefik
---
listen_addr: 192.168.1.68:7422
appsec_config: crowdsecurity/appsec-default
name: traefikproxy
source: appsec
labels:
  type: appsec

📝 Final Notes

  • Replace all external and internal domain names (plex.randomdomain.com, www.randomdomain.com, homelab.home) with your actual domains – internal and external.
  • Ensure CrowdSec is running on its own macvlan network (see createmediamacvlan.sh).
  • While this guide highlights how to integrate CrowdSec into Traefik, it does not cover setting up the CrowdSec LAPI at all, and very briefly covers setting up this CrowdSec Agent which is running as a Parser and an Appsec as well
  • IMPORTANT! You likely will get specific IPv6 addresses that are unique and different from the ones you specified, when I learn this pattern I will update this post, but as of yet the best action is to find out what those are, do not change your container MAC addresses and update your IPv6 addresses where necessary and when you deploy new, the host will keep said IPv6 address
  • Please see my and other how-tos on setting up CrowdSec

🛠️ To bring up the Docker environment

docker compose pull && docker compose up -d

📚 Resources

Let me know if this helps you of if a part could benefit from additional explanation! 🚀

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.