Traefik v3 and CrowdSec with Docker Compose: A Modern Security Stack
Table of Contents 📑
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 |
---|---|
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
80
and443
sudo
or root access
You can install the required utilities with:
sudo apt update
sudo apt install -y curl openssl apache2-utils
2. 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.json
This 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
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
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.5
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
fields:
headers:
defaultMode: keep
EOF
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"
EOF
3.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.1
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", "version"]
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
3.5. Create Dashboard Password
sudo htpasswd -c traefik/dynamic/.htpasswd admin
3.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
EOF
4. 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:
Copy the long, alphanumeric string that is output.docker compose exec crowdsec cscli bouncers add traefik -o raw
- Update your
.env
file: 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 list
traefik
bouncer 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:10.6
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=true"
- "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. |
6. Maintenance
To update your stack to the latest container images:
cd /opt/containers/traefik-stack
docker compose pull
docker compose up -d --remove-orphans
Log Rotation
Long-running setups should configure log rotation for Traefik and system logs to prevent disks from filling up. This can be done with logrotate
or by using Docker’s built-in logging drivers.
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.