Traefik v3 and CrowdSec with Docker Compose: A Modern Security Stack

Table of Contents 📑
- Changelog
- 1. Prerequisites
- 2. Directory Structure
- 3. Configuration Files
- 3.1. Main Configuration (.env)
- 3.2. Traefik Static Configuration (traefik.yml)
- 3.2.1. Forwarded Headers (Trusted IPs)
- 3.3. Traefik Dynamic Configuration (dynamic/middlewares.yml)
- 3.4. Docker Compose (docker-compose.yml)
- 3.5. Create Dashboard Password
- 3.6. CrowdSec Acquisition Configuration (crowdsec/config/acquis.yaml)
- 4. Launch and Verify the Stack
- 5. Adding Services to Traefik
- 6. Maintenance
- 7. Troubleshooting: Disk Full
- Conclusion
This guide provides a streamlined approach to deploying a powerful and secure web stack using Traefik as a reverse proxy and CrowdSec for threat protection. We will use Docker Compose to orchestrate the services, creating a setup that is easy to manage, scale, and maintain.
Changelog
| Date | Change |
|---|---|
| 2026-03-03 | Security Fix: Added forwardedHeaders.trustedIPs to both entrypoints to prevent X-Forwarded-For spoofing. |
| 2026-03-03 | Component Updates & Syntax Fixes: Updated Traefik to v3.6, CrowdSec plugin to v1.4.7, and pinned CrowdSec to v1.6. Fixed Traefik v3 HostRegexp syntax and improved healthchecks. |
| 2026-01-26 | Robustness Update: Added AccessLog filtering to prevent disk exhaustion and improved logrotate instructions with path discovery. |
| 2026-01-15 | Docs Update: Removed redundant router.tls=true label from the bypass example and added a new section explaining how to route multiple (sub)domains to one service. |
| 2025-12-10 | Production Hardening: Added comprehensive log rotation configuration and disk troubleshooting section based on real-world feedback. |
| 2025-09-18 | Final Review: Switched to robust httpChallenge, corrected provider in example, added raw API key generation. |
| 2025-09-17 | Major Refactor: Switched from legacy bouncer container to modern Traefik Plugin. Moved all variable configs to Docker Labels. |
| 2025-07-10 | Added Special Use-Case: Included a clear example of how to deploy a service without CrowdSec protection for specific needs. |
| 2025-07-09 | Initial Version: Article created based on best practices for a modern Traefik v3 and CrowdSec deployment with Docker Compose. |
1. Prerequisites
Before you begin, ensure you have the following installed on your server (e.g., Ubuntu 22.04 or Debian 12):
- Docker and Docker Compose
curl,openssl, andapache2-utils(for password generation)- A domain name pointed to your server’s IP address
- Open firewall ports
80and443 sudoor root access
You can install the required utilities with:
sudo apt update
sudo apt install -y curl openssl apache2-utils2. Directory Structure
A well-organized directory structure is key. We will create a central location for our stack’s configuration and data.
# Create the main directory
sudo mkdir -p /opt/containers/traefik-stack
cd /opt/containers/traefik-stack
# Create directories for each service
sudo mkdir -p traefik/{dynamic,logs,certs}
sudo mkdir -p crowdsec/{config,data}
# Prepare Let's Encrypt certificates file
sudo touch traefik/certs/acme.json
sudo chmod 600 traefik/certs/acme.jsonThis structure keeps everything tidy and separated.
3. Configuration Files
Now, let’s create the configuration files for our stack.
3.1. Main Configuration (.env)
This file holds all your environment-specific variables.
sudo tee .env > /dev/null << 'EOF'
# --- General Settings ---
TZ=Europe/Berlin
DOMAIN_NAME=your-domain.com
# --- Traefik Settings ---
TRAEFIK_DASHBOARD_HOST=traefik.${DOMAIN_NAME}
LETSENCRYPT_EMAIL=your-email@example.com
# --- CrowdSec Settings ---
CROWDSEC_COLLECTIONS="crowdsecurity/traefik crowdsecurity/linux"
# --- Credentials ---
CROWDSEC_BOUNCER_API_KEY=PASTE-YOUR-GENERATED-KEY-HERE
EOF| ⚠️ IMPORTANT |
Replace |
3.2. Traefik Static Configuration (traefik.yml)
This file contains the core Traefik settings that rarely change.
sudo tee traefik.yml > /dev/null << 'EOF'
global:
checkNewVersion: true
sendAnonymousUsage: false
api:
dashboard: true
ping: {}
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
forwardedHeaders:
insecure: false
trustedIPs:
- "127.0.0.1/32"
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
websecure:
address: ":443"
forwardedHeaders:
insecure: false
trustedIPs:
- "127.0.0.1/32"
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /etc/traefik/dynamic
watch: true
experimental:
plugins:
crowdsec-bouncer:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.4.7
certificatesResolvers:
tls_resolver:
acme:
storage: /certs/acme.json
httpChallenge:
entryPoint: web
log:
level: INFO
filePath: /var/log/traefik/traefik.log
accessLog:
filePath: /var/log/traefik/access.log
format: json
filters:
statusCodes:
- "400-599"
fields:
headers:
defaultMode: keep
EOF3.2.1. Forwarded Headers (Trusted IPs)
Traefik acts as a reverse proxy in front of your applications. To ensure your apps see the real client IP (and not the internal Docker gateway IP), Traefik must correctly process the X-Forwarded-For header.
Without this configuration, any visitor can spoof the X-Forwarded-For header, bypassing IP-based rate limiting, abuse protection, or geo-blocking.
What do these entries in traefik.yml mean?
insecure: false— Traefik does not automatically trust all incomingX-Forwarded-Forheaders. This is the default, but we set it explicitly for clarity.trustedIPs— Traefik only accepts the forwarded header from these source IPs. For all other requests, Traefik overwrites the header with the actual sender IP.- The four ranges cover the loopback interface (127.0.0.1) and all private network ranges (used for internal Docker communication).
| ℹ️ UPSTREAM PROXIES |
If you run a load balancer or CDN in front of Traefik, you must add its IP ranges as well. Examples:
Without the correct IPs, your application will see the load balancer’s IP instead of the client’s IP. |
| ⚠️ NEVER USE INSECURE: TRUE |
The setting |
| ℹ️ PLUGIN NAMING CONVENTION |
The plugin is defined here under the name |
3.3. Traefik Dynamic Configuration (dynamic/middlewares.yml)
This file defines reusable middleware components.
sudo tee traefik/dynamic/middlewares.yml > /dev/null << 'EOF'
http:
middlewares:
# 1. General security headers
security-headers:
headers:
browserXssFilter: true
contentTypeNosniff: true
frameDeny: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
# 2. Basic Auth for the dashboard
traefik-dashboard-auth:
basicAuth:
usersFile: "/etc/traefik/dynamic/.htpasswd"
EOF3.4. Docker Compose (docker-compose.yml)
This is the main file defining our services.
sudo tee docker-compose.yml > /dev/null << 'EOF'
services:
traefik:
image: traefik:v3.6
container_name: traefik
restart: unless-stopped
ports:
- "80:80"
- "443:443"
command:
- --certificatesresolvers.tls_resolver.acme.email=${LETSENCRYPT_EMAIL}
networks:
- proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/dynamic:/etc/traefik/dynamic:ro
- ./traefik/certs:/certs
- ./traefik/logs:/var/log/traefik
environment:
- TZ=${TZ}
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer.crowdsecMode=stream"
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer.crowdsecLapiHost=crowdsec:8080"
- "traefik.http.middlewares.crowdsec-bouncer.plugin.crowdsec-bouncer.crowdsecLapiKey=${CROWDSEC_BOUNCER_API_KEY}"
- "traefik.http.routers.dashboard.rule=Host(`${TRAEFIK_DASHBOARD_HOST}`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=tls_resolver"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=traefik-dashboard-auth@file,crowdsec-bouncer@docker"
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 30s
timeout: 10s
retries: 3
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
networks:
- proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./crowdsec/config:/etc/crowdsec:z
- ./crowdsec/data:/var/lib/crowdsec/data:z
- ./traefik/logs:/var/log/traefik:ro
- /var/log:/var/log/host/:ro
environment:
- TZ=${TZ}
- COLLECTIONS=${CROWDSEC_COLLECTIONS}
healthcheck:
test: ["CMD", "cscli", "version"]
interval: 60s
timeout: 15s
retries: 3
networks:
proxy:
name: proxy
external: true
EOF| ⚠️ DOCKER SOCKET SECURITY |
Mounting |
3.5. Create Dashboard Password
sudo htpasswd -c traefik/dynamic/.htpasswd admin3.6. CrowdSec Acquisition Configuration (crowdsec/config/acquis.yaml)
sudo tee crowdsec/config/acquis.yaml > /dev/null << 'EOF'
filenames:
- /var/log/traefik/access.log
labels:
type: traefik
---
filenames:
- /var/log/host/auth.log
- /var/log/host/syslog
labels:
type: syslog
EOF4. Launch and Verify the Stack
- Create the external network:
docker network create proxy - Start the services:
Upon first launch, Traefik will request a certificate from Let’s Encrypt. This may take a minute.docker compose up -d - Generate the Bouncer API Key:
docker compose exec crowdsec cscli bouncers add traefik -o raw
Copy the long, alphanumeric string that is output.ℹ️ LAPI STARTUP TIME Upon the very first start, CrowdSec might take a few moments to initialize the Local API (LAPI). If you get a connection error, wait 10-20 seconds and try the command again.
- Update your
.envfile: Paste the copied key as the value forCROWDSEC_BOUNCER_API_KEY. - Restart Traefik to apply the key:
docker compose up -d --force-recreate traefik - Verify the bouncer connection:
You should see thedocker compose exec crowdsec cscli bouncers listtraefikbouncer listed. You can now access your dashboard athttps://traefik.your-domain.com.
5. Adding Services to Traefik
Here is how to expose other Docker services through Traefik, with and without CrowdSec protection.
5.1. Scenario 1: Service Protected by CrowdSec (Standard)
This is the recommended setup for most public-facing services. We’ll use WordPress as an example. Create a docker-compose.yml in a separate directory (e.g., /opt/containers/wordpress):
services:
wordpress:
image: wordpress:latest
restart: unless-stopped
networks:
- default
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.wordpress.rule=Host(`blog.your-domain.com`)"
- "traefik.http.routers.wordpress.entrypoints=websecure"
- "traefik.http.routers.wordpress.tls.certresolver=tls_resolver"
- "traefik.http.routers.wordpress.middlewares=security-headers@file,crowdsec-bouncer@docker"
- "traefik.http.services.wordpress.loadbalancer.server.port=80"
db:
image: mariadb:11
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: change-me
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: supersecret
volumes:
- db_data:/var/lib/mysql
networks:
- default
networks:
proxy:
external: true
default:
volumes:
db_data:The key label is traefik.http.routers.wordpress.middlewares=security-headers@file,crowdsec-bouncer@docker, which applies our security headers (from the file provider) and the CrowdSec bouncer (defined on the Traefik service via Docker labels).
5.2. Scenario 2: Service Bypassing CrowdSec (Special Case)
Sometimes you need to expose a service without CrowdSec’s interference. Common reasons include:
- A public API that must not be blocked.
- A site heavily reliant on ad network traffic, where false positives could impact revenue.
- Internal tools that are already secured by other means.
To bypass CrowdSec, simply omit the crowdsec-bouncer@docker middleware.
# In the labels for your service (e.g., an API)
labels:
- "traefik.enable=true"
- "traefik.http.routers.my-api.rule=Host(`api.your-domain.com`)"
- "traefik.http.routers.my-api.entrypoints=websecure"
- "traefik.http.routers.my-api.tls.certresolver=tls_resolver"
# --- Middlewares (Security ONLY) ---
- "traefik.http.routers.my-api.middlewares=security-headers@file"
- "traefik.http.services.my-api.loadbalancer.server.port=3000"| ℹ️ MAXIMUM FLEXIBILITY |
By applying middleware on a per-router basis instead of globally on the entrypoint, you gain complete control over which services are protected by CrowdSec and which are not. |
5.3. Routing Multiple (Sub)Domains to One Service
Sometimes one container should respond to more than one hostname (e.g., example.com and www.example.com, or multiple subdomains pointing to the same app). You have a few common options.
Option A: Multiple Explicit Hosts in One Router
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`example.com`) || Host(`www.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=tls_resolver"
- "traefik.http.routers.app.middlewares=security-headers@file,crowdsec-bouncer@docker"
- "traefik.http.services.app.loadbalancer.server.port=80"Option B: Wildcard / Pattern Matching (Many Subdomains)
Use this if you want to match many subdomains dynamically.
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=HostRegexp(`[a-z0-9-]+\.example\.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=tls_resolver"
- "traefik.http.routers.app.middlewares=security-headers@file,crowdsec-bouncer@docker"
- "traefik.http.services.app.loadbalancer.server.port=80"| ⚠️ WILDCARD CERTIFICATES & LET'S ENCRYPT |
While |
Option C: Multiple Routers, One Service (Different Middleware per Host)
This is useful if one hostname should bypass CrowdSec while another one stays protected.
labels:
- "traefik.enable=true"
- "traefik.http.routers.app-main.rule=Host(`app.example.com`)"
- "traefik.http.routers.app-main.entrypoints=websecure"
- "traefik.http.routers.app-main.tls.certresolver=tls_resolver"
- "traefik.http.routers.app-main.middlewares=security-headers@file,crowdsec-bouncer@docker"
- "traefik.http.routers.app-api.rule=Host(`api.example.com`)"
- "traefik.http.routers.app-api.entrypoints=websecure"
- "traefik.http.routers.app-api.tls.certresolver=tls_resolver"
- "traefik.http.routers.app-api.middlewares=security-headers@file"
- "traefik.http.services.app.loadbalancer.server.port=80"
- "traefik.http.routers.app-main.service=app"
- "traefik.http.routers.app-api.service=app"6. Maintenance
To update your stack to the latest container images:
cd /opt/containers/traefik-stack
docker compose pull
docker compose up -d --remove-orphans6.1. Log Rotation (Critical for Production)
| ⚠️ DON'T SKIP THIS |
This setup generates detailed JSON access logs for CrowdSec analysis. Without proper log rotation, Traefik logs alone can consume 70+ GB within weeks on a busy server. Docker container logs can add another 40+ GB. Configure rotation before going to production. |
Docker Global Log Rotation
Configure Docker to automatically rotate all container logs by editing /etc/docker/daemon.json:
sudo tee /etc/docker/daemon.json > /dev/null << 'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
EOFApply the changes:
sudo systemctl restart docker
docker info | grep -A3 "Logging Driver"| ℹ️ EXISTING CONTAINERS |
This setting only applies to newly created containers. Existing containers keep their old logging configuration. Recreate them with |
Traefik Host Log Rotation
Since Traefik writes logs directly to the host filesystem, use logrotate.
| ⚠️ PATH VERIFICATION |
Ensure the path in the config below matches your host path where the logs are stored. You can find it by running: |
sudo tee /etc/logrotate.d/traefik > /dev/null << 'EOF'
/opt/containers/traefik-stack/traefik/logs/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate
maxsize 50M
}
EOFVerify Log Rotation
To ensure your rotation is working correctly and to troubleshoot issues, use these commands:
# Debug: show which rules would apply without actually rotating
sudo logrotate -d /etc/logrotate.d/traefik
# Force rotation: execute immediately
sudo logrotate -f /etc/logrotate.d/traefik
# Check rotation status: see when logs were last rotated
sudo cat /var/lib/logrotate/status | grep "traefik"7. Troubleshooting: Disk Full
If your server runs out of disk space, logs are usually the culprit. Here’s how to diagnose and fix it.
7.1. Identify the Problem
# Quick overview
df -h
# Find large directories
du -h -d1 / 2>/dev/null | sort -h
du -h -d1 /var | sort -h
du -h -d1 /opt | sort -h
# Docker-specific
du -h -d1 /var/lib/docker | sort -h
du -h -d1 /var/lib/docker/containers | sort -h
# Find large container logs
find /var/lib/docker/containers -name '*-json.log' -exec du -h {} \; | sort -h
# Check journald (usually not the problem)
journalctl --disk-usage7.2. Emergency Cleanup
| ⚠️ CAUTION |
These commands delete log data permanently. Only proceed if you don’t need the logs for debugging. |
Truncate Docker container logs:
sudo find /var/lib/docker/containers -name '*-json.log' -exec truncate -s 0 {} \;Clear Traefik logs:
cd /opt/containers/traefik-stack
docker compose down
rm -rf traefik/logs/*
docker compose up -dAfter cleanup, immediately configure log rotation as described in section 6.1 to prevent recurrence.
Conclusion
You now have a modern, secure, and flexible reverse proxy setup. Traefik handles routing and TLS termination effortlessly, while CrowdSec provides a powerful, community-driven security layer. By managing middlewares on a per-service basis, you can tailor the level of protection to fit the exact needs of each application you deploy.
As a next step, you might want to explore the AppSec (WAF) capabilities of the CrowdSec Bouncer plugin, which adds application-level protection against advanced attacks like SQL injection and Cross-Site Scripting directly at the Traefik layer.





