Mastering Traefik v3 as a Reverse Proxy: Docker Configuration and Security

Dec 30, 2025·
Benjamin Rabiller
Benjamin Rabiller
· 6 min read

Traefik has established itself as the standard for container routing thanks to its dynamic discovery capability. In this article, we will dissect a Traefik v3.6 configuration, designed for a self-hosting environment, featuring automatic TLS encryption and authentication delegation.

The goal is not simply to copy-paste a YAML file, but to understand every directive, every label, and the underlying security strategy (specifically the use of a ForwardAuth).

1. Architecture and Key Concepts

Before analyzing the code, let’s recall Traefik’s role here: it acts as an Edge Router. It intercepts all incoming traffic (ports 80 and 443), handles TLS termination via Let’s Encrypt, and routes requests to the appropriate containers based on Docker Labels.

We will analyze the stack across three axes:

  1. Static configuration (the Traefik service itself).
  2. Security and Auth (the TinyAuth service).
  3. Application routing (how to expose your applications).

2. Commented Walkthrough: The Static Configuration

The static configuration defines how Traefik starts and connects to providers. Here is the analysis of the traefik service found in the docker-compose.yml.

The Traefik Service Block

services:
  traefik:
    image: traefik:v3.6
    container_name: traefik
    restart: always
    # ...

We are using version v3.6 here. It is a best practice to pin the minor version to avoid unexpected breaking changes during a docker pull. Remember, “Latest is not a version!”

The Commands (Flags)

This is where the proxy’s behavior is defined. Let’s group them by theme for readability:

a) Observability and API

    command:
      - --log.level=INFO
      - --metrics.prometheus=true
      - --api.insecure=true
      - --api=true
  • Logs & Metrics: The INFO level is standard. Enabling Prometheus allows scraping Traefik to monitor entrypoints and routers.
  • API & Dashboard: api.insecure=true enables the dashboard on port 8080 without authentication by default.
  • Point of attention: In production, we often prefer to disable insecure mode and route the dashboard through Traefik itself with an auth middleware. However, we will see below that access is restricted via port mapping.

b) Providers (Dynamic Discovery)

      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --providers.file.directory=/traefik_dynamic
      - --providers.file.watch=true
  • Docker: Traefik listens to the Docker socket.
  • ExposedByDefault=false: Crucial for security. By default, no container is exposed. You must explicitly add the label traefik.enable=true on each service you wish to publish.
  • File Provider: Allows loading dynamic configurations (global middlewares, custom TLS certificates) from the /traefik_dynamic directory.

c) EntryPoints and HTTPS Redirection

      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https

A canonical configuration: all traffic arriving on port 80 (web) is immediately redirected to 443 (websecure) via HTTPS. This forces a “Secure by Default” policy.

d) Certificates and ACME (Let’s Encrypt)

      - --certificatesresolvers.myresolver.acme.httpchallenge=true
      - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.myresolver.acme.caserver=[https://acme-v02.api.letsencrypt.org/directory](https://acme-v02.api.letsencrypt.org/directory)
      - --certificatesresolvers.myresolver.acme.email=benjamin.xxxxx@gmail.com
      - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
  • Resolver: Named myresolver.
  • Challenge: Uses httpChallenge. Traefik will temporarily serve a file on port 80 to prove to Let’s Encrypt that it owns the domain.
  • Technical note: The httpChallenge is simple but does not allow generating Wildcard certificates (*.domain.fr), which would require a dnsChallenge.
  • Storage: The acme.json file (persistent via volume) contains the private keys.

e) Access Logs (Specific Configuration)

      - --accesslog=false
      - --accesslog.format=json
      - --accesslog.fields.defaultmode=keep
      # ... (other filters)
  • Observation: We note a detailed field configuration (User-Agent kept, ServiceURL dropped), but the global directive is --accesslog=false.
  • Consequence: Currently, no access logs are generated. To enable them, simply switch this flag to true. I plan to retrieve the logs in json format and process them through an ELK stack.

Ports and Networks

    ports:
      - 80:80
      - 10.8.0.1:8080:8080/tcp
      - 443:443
  • Specific Binding: Note the line 10.8.0.1:8080:8080/tcp. This is a security measure compensating for the --api.insecure=true flag. The dashboard is only accessible via the network interface with IP 10.8.0.1 (my Wireguard VPN), and not exposed publicly.
    networks:
      - web
      - back

Traefik is connected to two networks:

  1. web: The public/DMZ network where front-ends are located.
  2. back: To reach services that should not be directly exposed but that Traefik must route.

3. Security with TinyAuth (ForwardAuth)

The compose file includes a lightweight authentication service: TinyAuth.

  tinyauth:
    image: ghcr.io/steveiliop56/tinyauth:v3
    # ... env vars ...
    labels:
      - traefik.enable=true
      # Router definition to access the TinyAuth interface
      - traefik.http.routers.tinyauth.rule=host(`tinyauthv2.bracloud.fr`)
      - traefik.http.routers.tinyauth.entrypoints=websecure
      - traefik.http.routers.tinyauth.tls.certresolver=myresolver
      
      # MIDDLEWARE definition
      - traefik.http.middlewares.tinyauth.forwardauth.address=http://tinyauth:3000/api/auth/traefik

The ForwardAuth Mechanism

This is where the magic happens. The last line of the labels defines a Middleware named tinyauth (implicitly attached to the service container, but globally usable).

The flow is as follows:

  1. A request arrives at a protected application.
  2. Traefik sees the tinyauth middleware.
  3. Traefik pauses the request and contacts http://tinyauth:3000/....
  4. If TinyAuth responds with 200 OK, Traefik lets the request pass to the application. Otherwise, it redirects to the login.

4. Integration Examples (How to use this stack?)

The configuration above is the foundation. Here is how to deploy applications behind this Traefik instance.

A. Minimal Example (Whoami)

To expose a simple service on the web network.

services:
  whoami:
    image: traefik/whoami
    networks:
      - web
    labels:
      # 1. Explicit activation (because exposedbydefault=false)
      - "traefik.enable=true"
      # 2. Routing rule
      - "traefik.http.routers.whoami.rule=Host(`whoami.bracloud.fr`)"
      # 3. HTTPS Entrypoint and Certificate
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls.certresolver=myresolver"

B. Advanced Example: Protected Internal Service

Imagine a sensitive business application (internal-app) located on the back network that we want to protect with TinyAuth.

services:
  webapp:
    image: my-company/app
    networks:
      - back
    labels:
      - "traefik.enable=true"
      # Tell Traefik which network to use to contact the container
      # (Necessary because Traefik is on 'web' and 'back', but the app is only on 'back')
      - "traefik.docker.network=back"
      
      - "traefik.http.routers.app-sec.rule=Host(`admin.bracloud.fr`)"
      - "traefik.http.routers.app-sec.entrypoints=websecure"
      - "traefik.http.routers.app-sec.tls.certresolver=myresolver"
      
      # APPLYING THE AUTH MIDDLEWARE
      # Syntax: middleware-name@provider
      - "traefik.http.routers.app-sec.middlewares=tinyauth@docker"

Important Note: The middleware is named tinyauth and was defined in the labels of the tinyauth container (therefore via the docker provider). We must call it via tinyauth@docker.


5. Vigilance Points & Debugging

If you deploy this stack, here are the critical points to verify to avoid “404 Page Not Found” or “Bad Gateway” errors.

  1. External Networks: The docker-compose.yml declares the web and back networks as external: true. You must create them before launching the stack:
docker network create web
docker network create back
  1. acme.json Permissions: The mounted file /etc/letsencrypt/acme.json (or the folder) must have correct permissions (usually chmod 600) so that Traefik can write private keys to it.
  2. DNS: The httpChallenge requires that the domain (e.g., tinyauthv2.bracloud.fr) points to the server’s public IP and that port 80 is open and redirected to this container.
  3. Port Conflict: Verify that no other process (Apache, System Nginx) is already listening on port 80 or 443 of the host machine.

Conclusion

This Traefik v3.6 configuration is robust. It cleanly separates responsibilities: Traefik handles cryptography and routing, TinyAuth handles identity, and Docker handles the application lifecycle.

The use of --providers.docker.exposedbydefault=false and binding the dashboard to a specific IP (10.8.0.1) demonstrates a “Security by Design” approach.

Benjamin Rabiller
Authors
DevOps/Cloud Architect

Actuellement ingénieur DevOps/Architecte Cloud, j’étais initialement interessé par l’administration système et grâce aux entreprises dans lesquelles j’ai pu travailler Oxalide et maintenant Claranet j’ai eu la chance de découvrir l’univers du Cloud et de l’automatisation.

Je me suis décidé a publier ce blog pour vous faire partager ma passion mais également pour enrichir avec modestie tout ce que l’on peut trouver sur internet. Bonne lecture !