Traefik Proxy for Docker Containers with Cloudflare Full Strict TLS

Exploring how to use Traefik for routing Docker containers while securely terminating traffic with Cloudflare Full Strict TLS. This PoC demonstrates setting up a clean internal Docker network, connecting multiple services like MinIO, and ensuring requests are routed correctly based on host rules.

Traefik Proxy for Docker Containers with Cloudflare Full Strict TLS
Photo by Fliesentisch Fotograf / Unsplash

My senior asked me to explore proxying Docker containers using Traefik. At that time, there was no existing setup yet, so everything had to start from zero. I also didn’t really ask too many questions at first. Turns out, a few simple apps were still running on Kubernetes, and that’s the main reason he wanted to migrate them to plain Docker instead.

He had already tried Nginx Proxy Manager before, so my task was to explore Traefik and Kong. Personally, for simple container setups, I usually just use Cloudflare Tunnel. It’s fast, less things to configure, and usually just works. If tunnel cannot work, then I fall back to plain Nginx.

The Nginx approach is simple and works fine for personal projects. Spin up, proxy, okay already. But production-grade? Not really lah. Below is an example of the kind of Nginx setup I often use when I just want something up quickly.

version: "3.9"
services:
  nginx:
    image: nginx:stable-alpine
    container_name: nginx-proxy
    network_mode: host
    volumes:
      - ./conf.d:/etc/nginx/conf.d:ro
    restart: always
server {
    listen 80;
    server_name api.anantafatur.dev;

    location / {
        proxy_pass http://127.0.0.1:9001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

PoC Goal

This PoC demonstrates routing Docker containers using Traefik with Cloudflare Full Strict TLS. Traefik was chosen simply because that was the task assigned.

The goal here is not benchmarking or performance tuning. I’m not trying to squeeze every last bit of CPU. The goal is very straightforward: make sure routing works properly, TLS terminates correctly, and nothing weird happens along the way. Once this part is confirmed, only then it makes sense to go further.


Traefik Setup

For this PoC, MinIO was used as the example application. Nothing special here, but realistic enough to test real-world routing and TLS termination.

Traefik runs inside Docker and handles Cloudflare Full Strict TLS using an origin certificate. A dedicated Docker network is used so Traefik and application containers can talk to each other internally. This way, not every container needs to expose ports directly to the host, which keeps things cleaner.

Folder Structure

traefik/
├── docker-compose.yml
├── traefik.yml
├── dynamic.yml
├── certs/
    ├── origin.crt
    └── origin.key

Folder Structure

Configuration is split into static (traefik.yml) and dynamic (dynamic.yml) files. Slightly more setup work at the start, but once done, it’s easier to manage when things grow bigger.

Docker Network and Traefik Compose

docker network create traefik-net
version: "3.9"
services:
  traefik:
    image: traefik:v3.6.7
    container_name: traefik
    restart: unless-stopped
    networks:
      - traefik-net
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./certs:/certs:ro
      - ./traefik.yml:/traefik.yml:ro
      - ./dynamic.yml:/dynamic.yml:ro
    command:
      - "--configFile=/traefik.yml"

networks:
  traefik-net:
    external: true

docker-compose.yaml


Traefik Configuration

The certs/origin.crt and certs/origin.key files are Cloudflare Origin Certificates, which you can generate and download from Cloudflare Origin CA. Traefik’s configuration is split into static and dynamic files: the dynamic file points Traefik to these certificates and enforces a minimum TLS version, while the static file sets up entry points, the dashboard, and Docker/file providers so Traefik can watch containers and TLS settings.

tls:
  certificates:
    - certFile: /certs/origin.crt
      keyFile: /certs/origin.key
  options:
    default:
      minVersion: VersionTLS12

dynamic.yaml

log:
  level: INFO
accessLog: {}
api:
  dashboard:
    insecure: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  docker:
    exposedByDefault: false
    network: traefik-net
  file:
    filename: /dynamic.yml
    watch: true

traefik.yml


MinIO Setup

For this PoC, MinIO was used to test Traefik routing. MinIO exposes two ports: 9001 for the console and 9000 for the S3 API.

Traefik routes traffic based on host rules. Console traffic goes to one hostname, API traffic goes to another. Very clear separation, easy to reason about when something cannot work.

There’s also a helper container that initialises buckets and access keys after a short delay. This is just to make sure MinIO is fully up before running any setup commands. Data persists using Docker volumes, and credentials are set using environment variables.

version: "3.9"
services:
  minio:
    image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
    container_name: minio
    restart: always
    networks:
      - traefik-net
    environment:
      MINIO_ROOT_USER: minioadmin
      MINIO_ROOT_PASSWORD: supersecret
    command: ["server", "--console-address", ":9001", "/data"]
    labels:
      - "traefik.enable=true"
      # MinIO Console
      - "traefik.http.routers.minio-console.rule=Host(`s3.anantafatur.dev`)"
      - "traefik.http.routers.minio-console.entrypoints=websecure"
      - "traefik.http.routers.minio-console.tls=true"
      - "traefik.http.routers.minio-console.service=minio-console-svc"
      - "traefik.http.services.minio-console-svc.loadbalancer.server.port=9001"
      # MinIO API
      - "traefik.http.routers.minio-api.rule=Host(`s3-api.anantafatur.dev`)"
      - "traefik.http.routers.minio-api.entrypoints=websecure"
      - "traefik.http.routers.minio-api.tls=true"
      - "traefik.http.routers.minio-api.service=minio-api-svc"
      - "traefik.http.services.minio-api-svc.loadbalancer.server.port=9000"

  minio-buckets:
    image: quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z
    container_name: minio-buckets
    depends_on:
      - minio
    networks:
      - traefik-net
    entrypoint: >
      /bin/sh -c "
      sleep 5;
      mc alias set localminio http://minio:9000 minioadmin supersecret;
      mc mb localminio/mybucket || true;
      mc admin accesskey create localminio/ --access-key accesskey123 --secret-key secretkey1234567890 || true;
      "
      
networks:
  traefik-net:
    external: true

docker-compose.yaml


Architecture Diagram

          ┌─────────────────────┐
          │   Cloudflare CDN    │
          │  (Full Strict TLS) │
          └─────────┬──────────┘
                    │
                    ▼
          ┌─────────────────────┐
          │       Traefik       │
          │  Docker Container   │
          │  EntryPoints: 80/443│
          └─────┬─────────┬─────┘
                │         │
        ┌───────▼──────┐  │
        │ MinIO Console│  │
        │   port 9001  │  │
        └──────────────┘  │
                          │
                  ┌───────▼───────┐
                  │   MinIO API   │
                  │   port 9000   │
                  └───────────────┘

Traefik terminates Cloudflare TLS using the Cloudflare origin certificate. All traffic to MinIO is routed internally over the traefik-net Docker network. Simple flow, no magic.


Resource Usage (Idle State)

The following Docker stats were captured while the environment was idle, with no active traffic routed through Traefik. This is included only to show baseline resource usage, not as a performance benchmark.

NAME      CPU %     MEM USAGE / LIMIT     MEM %
minio     0.02%     84.37MiB / 1.866GiB   4.42%
traefik   0.00%     17.15MiB / 1.866GiB   0.90%

docker stats

At idle, Traefik uses very little CPU and memory. Nothing shocking here. Which is good. Means it’s not burning resources while just sitting there doing nothing.


Conclusion

So far, this PoC works as expected. Routing is correct, TLS terminates cleanly with Cloudflare Full Strict mode, and everything stays neatly inside the Docker network.

No benchmarking or load testing done yet. That one can come later. For now, this setup is purely to confirm that Traefik can do its job without giving unnecessary headache. For that purpose, it’s good enough already.