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-08-11 | Enhancements: Added acme.json permissions step, improved API key handling, stronger security headers, healthchecks, caching hints, etc. |
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
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/{config,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. Important: Generate the CrowdSec API key beforehand and paste it into this file.
openssl rand -base64 32
Copy the output into CROWDSEC_BOUNCER_API_KEY below:
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. Docker Compose (docker-compose.yml
)
This file defines our three core services: Traefik, CrowdSec, and the Traefik Bouncer.
sudo tee docker-compose.yml > /dev/null << 'EOF'
version: '3.9'
services:
traefik:
image: traefik:v3.0
container_name: traefik
restart: unless-stopped
depends_on:
- crowdsec-bouncer
ports:
- "80:80"
- "443:443"
networks:
- traefik_proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/config/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/dynamic:/etc/traefik/dynamic:ro
- ./traefik/certs:/certs
- ./traefik/logs:/var/log/traefik
environment:
- TZ=${TZ}
healthcheck:
test: ["CMD", "traefik", "version"]
interval: 30s
timeout: 10s
retries: 3
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
networks:
- traefik_proxy
volumes:
- ./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
crowdsec-bouncer:
image: crowdsecurity/traefik-bouncer:latest
container_name: crowdsec-bouncer
restart: unless-stopped
depends_on:
- crowdsec
networks:
- traefik_proxy
environment:
- TZ=${TZ}
- CROWDSEC_BOUNCER_API_KEY=${CROWDSEC_BOUNCER_API_KEY}
- CROWDSEC_AGENT_HOST=crowdsec:8080
networks:
traefik_proxy:
name: traefik_proxy
external: true
EOF
💡 EXTERNAL NETWORK |
We define |
3.3. Traefik Static Configuration (traefik/config/traefik.yml
)
sudo tee traefik/config/traefik.yml > /dev/null << 'EOF'
global:
checkNewVersion: true
sendAnonymousUsage: false
# API and Dashboard
api:
dashboard: true
# Entrypoints
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
# Providers
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /etc/traefik/dynamic
watch: true
# Let's Encrypt Certificate Resolver
certificatesResolvers:
tls_resolver:
acme:
email: ${LETSENCRYPT_EMAIL}
storage: /certs/acme.json
tlsChallenge: {}
# Logging
log:
level: INFO
filePath: /var/log/traefik/traefik.log
accessLog:
filePath: /var/log/traefik/access.log
EOF
3.4. Traefik Dynamic Configuration (traefik/dynamic/middlewares.yml
)
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. CrowdSec Bouncer
crowdsec-bouncer:
forwardAuth:
address: http://crowdsec-bouncer:8080/api/v1/forwardAuth
trustForwardHeader: true
# 3. Basic Auth for the dashboard
traefik-dashboard-auth:
basicAuth:
usersFile: "/etc/traefik/dynamic/.htpasswd"
routers:
dashboard:
rule: "Host(`${TRAEFIK_DASHBOARD_HOST}`)"
service: api@internal
entryPoints:
- websecure
middlewares:
- traefik-dashboard-auth
- crowdsec-bouncer
tls:
certResolver: tls_resolver
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
docker network create traefik_proxy
docker-compose up -d
docker-compose ps
All three containers should be Up
and healthy.
To verify CrowdSec is working, check the bouncer list:
docker-compose exec crowdsec cscli bouncers list
You should see the traefik-bouncer
listed and validated. You can now access your Traefik dashboard at the URL you configured (e.g., https://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
):
version: '3.9'
services:
wordpress:
image: wordpress:latest
restart: unless-stopped
networks:
- default
- traefik_proxy
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: supersecret
WORDPRESS_DB_NAME: wordpress
labels:
- "traefik.enable=true"
# --- Routing ---
- "traefik.http.routers.wordpress.rule=Host(`blog.your-domain.com`)"
- "traefik.http.routers.wordpress.entrypoints=websecure"
# --- TLS ---
- "traefik.http.routers.wordpress.tls=true"
- "traefik.http.routers.wordpress.tls.certresolver=tls_resolver"
# --- Middlewares (Security + CrowdSec) ---
- "traefik.http.routers.wordpress.middlewares=security-headers@file,crowdsec-bouncer@file"
# --- Service Port ---
- "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:
traefik_proxy:
external: true
default:
volumes:
db_data:
The key label is traefik.http.routers.wordpress.middlewares=security-headers@file,crowdsec-bouncer@file
, which applies both our security headers and the CrowdSec bouncer.
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 do this, you simply omit the crowdsec-bouncer@file
middleware from the router’s labels.
# 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, e.g., via:
sudo apt install logrotate
Or use Docker’s logging drivers.
CrowdSec Bouncer Caching
The Traefik bouncer includes decision caching to reduce latency and load on the CrowdSec API. Refer to the bouncer documentation for the exact configuration keys supported by your version and enable caching as appropriate.
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.
{{ retro_button(url=“https://doc.traefik.io/traefik/”, text=“Traefik Documentation”, icon=“📚”) }} {{ retro_button(url=“https://docs.crowdsec.net/”, text=“CrowdSec Documentation”, icon=“🛡️”) }}