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:
- 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.
- The
createdmzmacvlan.sh
andcreatemediamacvlan.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! 🚀