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-10-12Add LAPI-first proxy maintenance and tested rollback guidance.
2026-10-11Fixed TLS/header protection and clarified embedded Go security updates.
2026-10-09Fixed proxy trust, selective blocking, credential exposure, network isolation and log safety; corrected setup examples.
2026-10-08Log Safety: Rotate access logs with USR1, bound process-log retention, and remove unsafe Docker-log truncation.
2026-07-17Related Guide: Linked the reusable Authelia SSO and 2FA deployment for Traefik-protected services.
2026-07-01Migration Hardening: Pinned production versions, clarified Docker socket and CrowdSec scope, and added ACME migration guidance.
2026-03-03Security Fix: Added forwardedHeaders.trustedIPs to both entrypoints to prevent X-Forwarded-For spoofing.
2026-03-03Component 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-26Robustness Update: Added AccessLog filtering to prevent disk exhaustion and improved logrotate instructions with path discovery.
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.

A container update and a host Go update are separate operations: these services embed their Go runtime at build time. Check traefik version and crowdsec -version as well as the application release. When Go publishes relevant security fixes, use a vendor image rebuilt with the patched Go version; installing a new Go compiler on the host does not repair an existing binary. Keep image digests and build provenance in your update records, and check the vendor advisories before declaring the security update complete. Go release history, Traefik version command.

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}
# Protect configuration and credentials created by the root-run examples below.
sudo chmod 700 crowdsec/config

# Prepare Let's Encrypt certificates file
sudo touch traefik/certs/acme.json
sudo chmod 600 traefik/certs/acme.json

These examples create root-owned files. Edit them with sudo, and keep the original administrative session available during setup. This is a new-stack procedure: inspect and back up existing configuration before replacing any file.

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}

# --- CrowdSec Settings ---
CROWDSEC_COLLECTIONS="crowdsecurity/traefik"
EOF
sudo chmod 600 .env
⚠️ IMPORTANT

Replace your-domain.com and set the ACME email directly in traefik.yml. The API key will be generated and stored in the protected middleware file in a later step.

3.2. Traefik Static Configuration (traefik.yml)

This file contains startup settings. Keep them in this file: do not mix ordinary startup CLI flags or environment settings with file configuration. The CLI --configFile selector is available when a non-default path is needed. Traefik configuration methods.

sudo tee traefik.yml > /dev/null << 'EOF'
global:
  checkNewVersion: false
  sendAnonymousUsage: false

core:
  strictTLSOptions: true

api:
  dashboard: true

ping: {}

entryPoints:
  web:
    address: ":80"
    http:
      aliasHeadersStrategy: delete
      redirections:
        entryPoint:
          to: websecure
          scheme: https
    forwardedHeaders:
      insecure: false
      # Direct Internet entrypoint: no upstream proxies are trusted by default.
      # Add only verified proxy addresses when a load balancer or CDN is used.
  websecure:
    address: ":443"
    http:
      aliasHeadersStrategy: delete
    forwardedHeaders:
      insecure: false
      # Direct Internet entrypoint: no upstream proxies are trusted by default.
      # Add only verified proxy addresses when a load balancer or CDN is used.

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: proxy
  file:
    directory: /etc/traefik/dynamic
    watch: true

experimental:
  plugins:
    crowdsec-bouncer:
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: v1.7.1

certificatesResolvers:
  tls_resolver:
    acme:
      email: "replace-with-your-acme-email@example.com"
      storage: /certs/acme.json
      httpChallenge:
        entryPoint: web

log:
  level: INFO
  filePath: /var/log/traefik/traefik.log
  maxSize: 50
  maxAge: 14
  maxBackups: 7
accessLog:
  filePath: /var/log/traefik/access.log
  format: json
  fields:
    headers:
      defaultMode: drop
      names:
        User-Agent: keep
        Authorization: drop
        Cookie: drop
        Set-Cookie: drop
EOF

Traefik 3.7.14 includes security fixes. The example removes header names containing aliasing characters before authentication middleware, so backends cannot reinterpret names such as X_Auth_User as a trusted identity header. Test clients that require unusual header names. strictTLSOptions disables routers with conflicting TLS policies rather than silently using the default policy; resolve conflicts before enabling it on an existing host. Review the Traefik migration notes and security release.

3.2.1. Forwarded Headers (Trusted IPs)

With direct Internet access, leave forwardedHeaders.trustedIPs unset and retain insecure: false. Trust belongs to a verified upstream proxy, not to every container or every private network. Broad RFC1918 ranges can let unrelated internal clients supply a forged client address. Traefik forwarded-header configuration.

If a load balancer or CDN sits in front of Traefik, add only its actual addresses or maintained official CIDRs to both entrypoints. For the CrowdSec plugin, configure forwardedHeadersTrustedIPs for the same upstream topology and test the client address recorded in access logs and used for decisions. Never solve a wrong client address by trusting arbitrary senders. Plugin configuration.

The access-log example retains request metadata and User-Agent while dropping other headers. Do not log credentials, cookies or tokens. Successful requests are included so CrowdSec has more than error responses to analyze. An error-only status filter reduces detection coverage; bound log retention instead, and review URL/query-string privacy for the application. Traefik access-log fields and filters.

⚠️ NEVER USE INSECURE: TRUE

The setting insecure: true causes Traefik to accept the X-Forwarded-For header from any source — even directly from the internet. This opens the door for IP spoofing and should never be used in production.

ℹ️ PLUGIN NAMING CONVENTION

The plugin is defined here under the name crowdsec-bouncer. You will often see the official CrowdSec documentation refer to it simply as bouncer. Both work perfectly, as long as you are consistent when referencing it in your Docker labels later on.

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:
        contentTypeNosniff: true
        referrerPolicy: strict-origin-when-cross-origin

    # 2. Basic Auth for the dashboard
    traefik-dashboard-auth:
      basicAuth:
        usersFile: "/etc/traefik/dynamic/.htpasswd"

    # 3. Selective CrowdSec enforcement, with a private API credential
    crowdsec-bouncer:
      plugin:
        crowdsec-bouncer:
          enabled: true
          crowdsecMode: stream
          updateMaxFailure: -1
          crowdsecLapiHost: crowdsec:8080
          crowdsecLapiKey: "PASTE-YOUR-GENERATED-KEY-HERE"
EOF
sudo chmod 600 traefik/dynamic/middlewares.yml

Security headers need application-specific testing. A blanket frameDeny can break embedded content or administration previews. Add CSP/frame policies and HSTS only after verifying application compatibility and HTTPS coverage; do not enable includeSubDomains or preload as a universal default. Traefik headers middleware.

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.7.14
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    networks:
      - proxy
      - crowdsec_api
    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.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@file"
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  crowdsec:
    image: crowdsecurity/crowdsec:v1.8.1
    container_name: crowdsec
    restart: unless-stopped
    networks:
      - crowdsec_api
      - crowdsec_egress
    volumes:
      - ./crowdsec/config:/etc/crowdsec:z
      - ./crowdsec/data:/var/lib/crowdsec/data:z
      - ./traefik/logs:/var/log/traefik:ro
    environment:
      - TZ=${TZ}
      - COLLECTIONS=${CROWDSEC_COLLECTIONS}
    healthcheck:
      test: ["CMD", "cscli", "lapi", "status"]
      interval: 60s
      timeout: 15s
      retries: 3
      start_period: 60s

networks:
  proxy:
    name: proxy
    external: true
  crowdsec_api:
    internal: true
  crowdsec_egress: {}
EOF
⚠️ DOCKER SOCKET SECURITY

A :ro mount does not make Docker API operations read-only. The direct socket mount is a privileged trust decision, not a fully isolated production default. Use a suitably restricted Docker socket proxy or another reviewed discovery method when that boundary is required; verify permitted endpoints and keep the proxy off application/public networks. CrowdSec reads bind-mounted log files here and does not need a Docker socket. Docker daemon security.

The LAPI is not published to the host or attached to the shared application proxy network. Traefik and CrowdSec share the internal crowdsec_api network; CrowdSec also has a separate egress network for hub downloads and central-API access. Attaching it only to an internal network would break that outbound access. Do not attach application containers to these private networks. Compose network isolation.

The bouncer key lives in a root-readable dynamic file rather than Docker labels. The pinned plugin uses crowdsecLapiKey; do not assume it supports an undocumented crowdsecLapiKeyFile option. The Traefik API/dashboard may still expose loaded plugin configuration, so restrict access to trusted administrators, avoid sharing raw API responses and protect configuration backups. Docker-API or root access remains privileged. Pinned plugin configuration.

ℹ️ CROWDSEC SCOPE

The Traefik bouncer enforces decisions only on routers that reference its middleware. Do not attach it globally to an entrypoint if some services must bypass it. An IP decision can affect every router using the bouncer, even if the triggering requests reached another application. Routers without it retain their own authentication and application protections.

A host-wide CrowdSec firewall bouncer can still block the same client before requests reach an exempt HTTP router. Review its rules separately before claiming a per-service bypass. SSH and other directly exposed protocols need their own controls; the HTTP plugin does not protect them. CrowdSec firewall bouncer.

The middleware explicitly uses updateMaxFailure=-1 in stream mode: failure to refresh decisions does not cause blanket blocking, at the cost of stale or missing protection while CrowdSec is unavailable. This does not clear cached IP bans or prevent false positives. Choose fail-open or fail-closed intentionally, monitor failures and test both LAPI outages and Traefik restarts with the pinned plugin version. Plugin initialization/startup behavior must be tested separately from a running-cache outage: do not assume updateMaxFailure=-1 makes a protected router available when Traefik starts without a reachable LAPI. Start or restore CrowdSec and verify LAPI authentication first; then start or recreate Traefik and test both protected and exempt routes. During initialization, a protected router can be temporarily unavailable even while exempt routes work. Verify actual route readiness after startup, not just container state. Pinned plugin failure policy.

3.5. Create Dashboard Password

sudo htpasswd -c traefik/dynamic/.htpasswd admin
sudo chmod 600 traefik/dynamic/.htpasswd

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
EOF

This base configuration acquires only Traefik access logs. Host/SSH acquisition is a separate optional collector: add its collection and a supported journal source, or route only the necessary events into a dedicated rotating log directory. Avoid granting this container read access to all of /var/log; mounting one rotating file can leave a stale inode. Verify input files and parser counters. Host SSH remains protected by the separately configured host controls, not this HTTP middleware.

4. Launch and Verify the Stack

  1. Create the external network:
    sudo docker network inspect proxy >/dev/null 2>&1 || sudo docker network create proxy
  2. Start the services:
    # Validate the file and initially start only the security engine.
    sudo docker compose config --quiet
    sudo docker compose up -d crowdsec
    sudo docker compose exec crowdsec cscli lapi status
    Wait for a successful LAPI authentication check before continuing. cscli version only checks the binary; it is not an API health check. CrowdSec LAPI status.
  3. Generate the Bouncer API Key:
    sudo docker compose exec crowdsec cscli bouncers add traefik -o raw
ℹ️ 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.
Copy the long, alphanumeric string that is output.
  1. Store the key in the middleware: Use sudoedit traefik/dynamic/middlewares.yml to replace the placeholder in crowdsecLapiKey. Keep mode 0600. Do not paste the key into Docker labels or share resolved configuration/API output.

  2. Start Traefik after storing the key:

    sudo docker compose up -d --force-recreate traefik
  3. Verify the bouncer connection:

    sudo docker compose exec crowdsec cscli bouncers list

    You should see the traefik bouncer with recent API activity, and reach the dashboard at https://traefik.your-domain.com. Certificate issuance starts when Traefik loads its TLS routers; allow time for ACME validation.

  4. Verify acquisition and the intended enforcement boundary:

    sudo docker compose exec crowdsec cscli metrics

    Send representative requests and confirm acquisition/parser counters increase. On a staging deployment, create a short-lived decision for a dedicated test client and verify that the protected router blocks it while an exempt router remains usable. Test logins, uploads, API clients and recovery, and remove the test decision afterwards. Never use an administrator’s sole access address for the test. CrowdSec metrics.

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 Explicitly Protected by CrowdSec

Apply this policy only after testing the application’s clients and agreeing on acceptable blocking behavior. WordPress is an example, not a recommendation to enable automatic IP blocking on every customer site. Availability-sensitive services such as sync clients, customer portals or public APIs may use the bypass policy below.

Create a separate project (e.g., /opt/containers/wordpress). In its private .env, set WORDPRESS_IMAGE and MARIADB_IMAGE to reviewed version-pinned image references, and generate distinct strong WORDPRESS_DB_PASSWORD and WORDPRESS_DB_ROOT_PASSWORD values. Keep the file mode 0600; the placeholders below deliberately fail until configured. Avoid sharing resolved Compose output, which can contain secrets.

services:
  wordpress:
    image: ${WORDPRESS_IMAGE:?Set a tested WordPress image reference}
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD:?Set the application database password}
    volumes:
      - wp_data:/var/www/html
    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@file"
      - "traefik.http.services.wordpress.loadbalancer.server.port=80"
  
  db:
    image: ${MARIADB_IMAGE:?Set a tested MariaDB image reference}
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${WORDPRESS_DB_ROOT_PASSWORD:?Set the database root password}
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: ${WORDPRESS_DB_PASSWORD:?Set the application database password}
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - default

networks:
  proxy:
    external: true
  default:

volumes:
  wp_data:
  db_data:

The key label is traefik.http.routers.wordpress.middlewares=security-headers@file,crowdsec-bouncer@file, which applies our security headers (from the file provider) and the CrowdSec bouncer (defined in the protected file-provider configuration).

5.2. Scenario 2: Service Without CrowdSec Blocking

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.

Omit crowdsec-bouncer@file from every router serving the application, including API, upload and alternate-host routes. Also check middleware chains and entrypoint defaults. Bypass is a deliberate availability policy: retain application updates, authentication, appropriate login throttling and monitoring. It is not an exemption from security maintenance.

# 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@file"
  - "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@file"
  - "traefik.http.services.app.loadbalancer.server.port=80"
⚠️ WILDCARD CERTIFICATES & LET'S ENCRYPT

HostRegexp matches requests but does not supply concrete domain names to ACME. A resolver obtains names from explicit tls.domains or Host()/HostSNI() rules; it does not issue a new certificate automatically for each visited regex match. Supply the intended names explicitly, or configure a wildcard certificate through DNS-01. HTTP-01 cannot issue wildcard certificates. Traefik ACME domain definition.

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@file"

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

6.1. Updating Pinned Versions

Treat a proxy update as a planned, reversible maintenance change. Check the actual candidate binaries, embedded Go/dependencies and plugin builds against current advisories; a vendor tag or successful HTTP response does not establish security acceptance. Keep known residual vulnerabilities explicit. Preserve the current enforcement policy: this maintenance does not enable new WAF or bot-detection features.

Before the window:

  • Save the original Compose file and overrides, private .env, Traefik static/dynamic configuration, ACME store with its permissions, and CrowdSec configuration/credentials. Record the old image IDs/digests and retain or export the exact old images. Keep rollback copies private and verify they can be restored.
  • Capture a consistent CrowdSec database backup. For SQLite, use its online backup API or a cleanly stopped engine with all database connections closed; copying only a live .db can miss WAL contents. Validate a restored isolated copy, not the production database, and retain the matching old configuration/image for schema rollback. Do not start a candidate against writable production state. SQLite backup API, SQLite WAL.
  • Pin the reviewed image/plugin versions, download the exact candidate images in advance, and exercise plugin initialization in an isolated candidate. Image pulls alone do not prefetch Traefik plugins. Prepare the required Hub rules and plugin artifacts, and test startup with the actual download/cache policy before entering the window.
  • Check CrowdSec’s required Hub and central-API connectivity. Startup downloads or unavailable remote services can delay readiness. NO_HUB_UPGRADE=true skips the startup Hub update/upgrade only where supported by the selected image; use it only with already prepared, verified compatible rules and an explicit later update plan. It does not promise offline startup or disable every installation/download. Do not change CAPI registration or production credentials in a candidate test. CrowdSec image configuration.
  • Test both protected and exempt routes plus legitimate application flows, authentication, APIs, uploads and WebSockets as applicable. Verify the bouncer’s real startup behavior against an isolated LAPI using test identities; do not reuse production writable mounts or send candidate signals/decisions to production. Define the acceptance deadline and rollback trigger before changing the public proxy.

Pull only the intended images during preparation, then perform the checked LAPI-to-proxy sequence in the window:

cd /opt/containers/traefik-stack
sudo docker compose ps -a
sudo docker compose config --quiet
# Preparation: download the pinned candidates before the maintenance window.
sudo docker compose pull crowdsec traefik

# Window: replace the proxy only after CrowdSec starts and LAPI auth succeeds.
if sudo docker compose up -d --no-deps crowdsec &&
   sudo docker compose exec -T crowdsec cscli lapi status; then
    sudo docker compose up -d --no-deps --force-recreate traefik
else
    printf 'CrowdSec/LAPI not ready; stop and diagnose before replacing Traefik.\n' >&2
    false
fi

A failed readiness check is a stop point, not permission to run the next command. Keep applications and their databases running; do not use a blanket stack up, down or restart for this proxy change. --no-deps limits dependency startup, and --force-recreate makes the intended proxy replacement explicit. Compose pull, Compose up, LAPI authentication check.

Recreating a single public proxy can interrupt active connections, including WebSockets; this sequence is not zero downtime. Immediately repeat the protected/exempt and legitimate application tests, verify TLS, client-address handling, bouncer activity, logs and the actual deployed versions/digests. If acceptance fails, restore the saved image/configuration set and, if CrowdSec changed its data schema, the matching consistent database backup with the engine stopped. Bring LAPI back to verified readiness before recreating the old proxy and repeat the same checks. Retain the rollback evidence until acceptance is complete.

6.2. Migrating acme.json

When moving this stack to a new server, decide deliberately whether to migrate the existing ACME store:

  • Copying traefik/certs/acme.json preserves existing certificates and avoids unnecessary Let’s Encrypt reissuance during cutover.
  • Starting with an empty acme.json is clean, but can hit rate limits if many domains are reissued repeatedly.

If you copy it, preserve permissions:

sudo chown root:root traefik/certs/acme.json
sudo chmod 600 traefik/certs/acme.json

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

Set bounded defaults before creating containers. Merge these keys into the existing /etc/docker/daemon.json; do not overwrite unrelated settings such as live-restore, storage, networking or registry configuration. Back up the file first. The local driver has efficient bounded storage; choose json-file instead only when another tool explicitly requires that format. Docker logging configuration.

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Validate the merged configuration:

sudo dockerd --validate --config-file=/etc/docker/daemon.json

Schedule any required Docker restart for a maintenance window after checking the running containers and recovery plan. Verify the resulting driver with docker info. Defaults apply only to newly created containers; explicit Compose logging settings take precedence. Recreate only reviewed services after checking persistent volumes, backups and expected downtime. Never force-recreate the whole production host just to update logging defaults.

Traefik Host Log Rotation

For file-based access logs, use host logrotate and send USR1 after renaming the file. Traefik reopens its access log without restarting the proxy. Process logs (log.filePath) have separate native rotation settings such as log.maxSize and log.maxBackups; do not include them in the access-log wildcard. Traefik v3.6 logging reference.

⚠️ PATH VERIFICATION

Ensure the path in the config below matches your host path where the logs are stored. You can find it by running: docker inspect traefik --format '{{range .Mounts}}{{if eq .Destination "/var/log/traefik"}}{{.Source}}{{"\n"}}{{end}}{{end}}'

sudo tee /etc/logrotate.d/traefik > /dev/null << 'EOF'
/opt/containers/traefik-stack/traefik/logs/access.log {
  daily
  rotate 7
  compress
  delaycompress
  missingok
  notifempty
  create 0600 root root
  maxsize 50M
  postrotate
    running=$(/usr/bin/docker ps -q --filter 'name=^/traefik$' --filter status=running) || exit 1
    if [ -n "$running" ]; then
      /usr/bin/docker kill --signal=USR1 traefik > /dev/null
    fi
  endscript
}
EOF

Use the actual container name and log-file owner/group. Match the access log to exactly one rule, and bind-mount the log directory so the renamed file and its replacement are visible inside the container. Avoid copytruncate for access logs: writes can be lost between copying and truncating. maxsize is checked only when logrotate runs; choose a timer frequency and disk reserve that fit the traffic. Preserve existing large logs during the first rotation.

Verify Log Rotation

After the scoped rotation, send a normal request and check that the new access log grows and the container has the same start time.

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

# 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
sudo du -x -h -d1 / 2>/dev/null | sort -h
sudo du -x -h -d1 /var | sort -h
sudo du -x -h -d1 /opt | sort -h

# Docker-specific
sudo du -x -h -d1 /var/lib/docker | sort -h
sudo du -x -h -d1 /var/lib/docker/containers | sort -h

# Account for JSON-file and local-driver storage, including rotations
sudo find /var/lib/docker/containers -type f \( -name '*-json.log' -o -path '*/local-logs/*' \) -exec du -h {} \; | sort -h

# Check journald (usually not the problem)
sudo journalctl --disk-usage

7.2. Recover Space Safely

Preserve logs required for incident analysis and identify the affected service before changing it. Do not externally truncate or delete Docker-managed *-json.log files: Docker reserves them for its daemon, and external changes can disrupt logging. Configure the logging driver’s max-size and max-file limits instead. Existing containers need controlled recreation to adopt new limits; verify persistent volumes, the Compose configuration and backups, and schedule the affected service’s outage first. Docker JSON-file logging documentation.

For Traefik access logs, use the scoped rotation from section 6.3 and verify continued writes after USR1. Avoid clearing the entire log directory or stopping the whole stack to rotate one file. Rotation can require extra space for retained or compressed files; if the filesystem is already full, add temporary capacity or move a specifically reviewed inactive archive to verified external storage before proceeding.

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.

To add a central login with TOTP or WebAuthn to browser-based services, continue with Authelia with Docker Compose and Traefik: Lightweight SSO and 2FA. It reuses the proxy network, TLS resolver and security headers. When adapting an existing example, use this guide’s crowdsec-bouncer@file provider reference consistently; older examples may define it through Docker labels.

📚TRAEFIK DOCUMENTATION 🛡️CROWDSEC DOCUMENTATION 🔐ADD AUTHELIA SSO + 2FA

Sources