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

DateChange
2026-01-15Docs 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-10Production Hardening: Added comprehensive log rotation configuration and disk troubleshooting section based on real-world feedback.
2025-09-18Final Review: Switched to robust httpChallenge, corrected provider in example, added raw API key generation.
2025-09-17Major Refactor: Switched from legacy bouncer container to modern Traefik Plugin. Moved all variable configs to Docker Labels.
2025-07-10Added Special Use-Case: Included a clear example of how to deploy a service without CrowdSec protection for specific needs.
2025-07-09Initial 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, and apache2-utils (for password generation)
  • A domain name pointed to your serverโ€™s IP address
  • Open firewall ports 80 and 443
  • 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 your-domain.com, your-email@example.com. The API key will be generated and pasted in a later step.

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

  1. Create the external network:
    docker network create proxy
  2. Start the services:
    docker compose up -d
    Upon first launch, Traefik will request a certificate from Letโ€™s Encrypt. This may take a minute.
  3. Generate the Bouncer API Key:
    docker compose exec crowdsec cscli bouncers add traefik -o raw
    Copy the long, alphanumeric string that is output.
  4. Update your .env file: Paste the copied key as the value for CROWDSEC_BOUNCER_API_KEY.
  5. Restart Traefik to apply the key:
    docker compose up -d --force-recreate traefik
  6. Verify the bouncer connection:
    docker compose exec crowdsec cscli bouncers list
    You should see the traefik bouncer listed. You can now access your dashboard at 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):

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.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(`{subdomain:[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"

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-orphans

6.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"
  }
}
EOF

Apply 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 docker compose up -d --force-recreate to apply the new limits.

Traefik Host Log Rotation

Since Traefik writes logs directly to the host filesystem, use logrotate:

sudo tee /etc/logrotate.d/traefik > /dev/null << 'EOF'
/opt/containers/traefik-stack/traefik/logs/*.log {
  daily
  rotate 7
  compress
  missingok
  notifempty
  copytruncate
  maxsize 50M
}
EOF

Test the configuration:

sudo logrotate -d /etc/logrotate.d/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-usage

7.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 -d

After 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.

๐Ÿ“šTRAEFIK DOCUMENTATION ๐Ÿ›ก๏ธCROWDSEC DOCUMENTATION