Buy Me a Coffee

[Docker/Traefik] A Production-Ready Traefik Setup with Docker Compose

Traefik setup with Docker Compose

Running multiple services on a single virtual machine (VM) can quickly become a logistical challenge, particularly when it comes to managing access and ensuring security. A reverse proxy, therefore, becomes a central piece of infrastructure. Traefik stands out among the options, offering native Docker integration, automatic HTTPS using Let’s Encrypt, dynamic routing via labels, and minimal manual configuration. But while Traefik boasts many advantages, achieving a clean, production-ready setup requires more than a simple “it works” configuration. This article will walk through structuring a robust Traefik setup on a VM, highlight key configuration choices, and delve into a subtle trap involving basic authentication and Docker Compose that could easily trip you up.

The Context: What We Actually Need

In any serious deployment, a reverse proxy must do more than just channel requests to the correct service. It should automatically manage HTTPS termination — converting secure, encrypted external traffic to the format required by an internal service. It also has to correctly route traffic based on domain names, effectively navigating multiple endpoints. Furthermore, it is crucial for security to avoid exposing services unnecessarily; only those intended to be accessible from outside the VM should be available. Additionally, the setup must allow for easy migration and backups — any administrator should be able to transition services to a new host with minimal downtime. Lastly, debugging should be straightforward, with predictable behavior that aligns with operational scenarios. Many online examples achieve basic functionality but fall short in these critical areas.

A Practical Design

For an efficient and manageable Traefik setup, relying heavily on Docker volumes and scattered configurations is not ideal. A centralized structure not only streamlines management tasks but also enhances portability.

Directory Structure

A thoughtful directory structure significantly contributes to smooth operations. By centralizing all Traefik-related data under a single directory, such as /data/traefik, you simplify backups and migrations:

/data/traefik/
├── config.yaml
└── letsencrypt/
    └── acme.json

This design places all necessary files for Traefik’s operation under one easily accessible path. The config.yaml provides a clear place for static configurations, while acme.json stores Let’s Encrypt certificate data. This organization means no hidden layers of Docker-managed states, ensuring everything is observable directly from the host filesystem.

Why This Matters

A unified directory strategy allows backups to be straightforward and reliable. Migrating services to another VM can be as simple as copying the /data/traefik directory, mitigating otherwise complex redeployment processes. Additionally, visibility into all configuration and state means troubleshooting can happen without digging through obscure container states.

Host Preparation

Before deploying Traefik, prepare your VM environment to ensure everything operates smoothly. Preparation involves creating necessary directories and setting the appropriate file permissions. Start by executing the following commands:

sudo mkdir -p /data/traefik/letsencrypt
sudo chown -R ubuntu:ubuntu /data/traefik

touch /data/traefik/letsencrypt/acme.json
chmod 600 /data/traefik/letsencrypt/acme.json

Each step is crucial, especially setting the correct permissions on acme.json, as improper settings will cause Let’s Encrypt’s certificate request to fail without explicit error messages. By granting secure, yet accessible permissions, Traefik can read and write as needed without exposing sensitive information to unauthorized users.

The Compose Configuration (Production-Oriented)

A well-crafted Docker Compose configuration is essential for maintaining a clean and efficient container setup. Below is a configuration setup that aligns with production needs:

 1version: "3.9"
 2
 3networks:
 4  proxy:
 5    name: traefik_proxy
 6    driver: bridge
 7    ipam:
 8      config:
 9      - subnet: 172.24.0.0/16
10
11services:
12  traefik:
13    image: traefik:v2.11
14    restart: unless-stopped
15
16    networks:
17    - proxy
18
19    command:
20      - --global.checknewversion=false
21      - --global.sendanonymoususage=false
22
23      - --api.dashboard=true
24      - --api.insecure=false
25
26      - --log.level=INFO
27      - --accesslog=true
28
29      - --ping=true
30
31      - --providers.docker=true
32      - --providers.docker.exposedbydefault=false
33      - --providers.docker.network=traefik_proxy
34
35      - --providers.file.filename=/etc/traefik/config.yaml
36      - --providers.file.watch=true
37
38      - --entrypoints.web.address=:80
39      - --entrypoints.web.http.redirections.entryPoint.to=websecure
40      - --entrypoints.web.http.redirections.entryPoint.scheme=https
41
42      - --entrypoints.websecure.address=:443
43
44      - --certificatesresolvers.leresolver.acme.email=your-email@example.com
45      - --certificatesresolvers.leresolver.acme.storage=/data/letsencrypt/acme.json
46      - --certificatesresolvers.leresolver.acme.tlschallenge=true
47
48    ports:
49      - "80:80"
50      - "443:443"
51
52    volumes:
53      - /var/run/docker.sock:/var/run/docker.sock:ro
54      - /data/traefik/config.yaml:/etc/traefik/config.yaml:ro
55      - /data/traefik/letsencrypt:/data/letsencrypt
56
57    labels:
58      - traefik.enable=true
59      - traefik.http.routers.traefik.rule=Host(`traefik.your-domain.com`)
60      - traefik.http.routers.traefik.entrypoints=websecure
61      - traefik.http.routers.traefik.tls.certresolver=leresolver
62      - traefik.http.routers.traefik.service=api@internal
63      - traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$abc123$$XYZ...
64      - traefik.http.routers.traefik.middlewares=traefik-auth

Why This Configuration Works Well

Let’s unpack why these specific settings in your Docker Compose file are essential for a robust deployment:

1. Containers are not exposed by default

One of the most critical security features of a Traefik setup is the command flag:

--providers.docker.exposedbydefault=false

By default, Docker tends to expose all services, which could lead to inadvertent exposures of sensitive services. This flag ensures that only those services explicitly labeled as public-facing in the configuration become accessible, offering a baseline security level right from the start. It’s a necessary measure to prevent accidental openings of your infrastructure to external threats.

2. HTTPS is enforced at the entry point level

Instead of requiring each service to handle HTTPS redirection, the entry point configuration does this at a global level:

--entrypoints.web.http.redirections.entryPoint.to=websecure

This simplifies the configuration, reducing redundancy and lowering the risk of accidental HTTP exposure. By enforcing HTTPS globally, you ensure all services follow the same security protocol without additional middleware, making configuration management more straightforward.

3. Docker socket is read-only

Mount the Docker socket as read-only:

/var/run/docker.sock:/var/run/docker.sock:ro

Traefik needs access to the Docker API to function correctly, but write access is unnecessary and can introduce security risks if something goes awry. Restricting access to read-only mitigates potential damage if the Traefik instance is compromised, limiting what an attacker could change within the Docker environment.

4. Data is stored in /data

Centralizing data under /data/traefik:

/data/traefik/...

Avoid dispersing state and configuration across Docker volumes or hidden locations. Housing it under /data makes your state concrete, portable, and easy to understand. This means less time spent on blind debugging and more predictability when managing transitions or troubleshooting issues.

5. Let’s Encrypt is predictable

Allowing for clear certificate management through:

--certificatesresolvers.leresolver.acme.storage=/data/letsencrypt/acme.json

Certificates are critical to service credibility. By specifying their exact location and handling, you gain immediate knowledge over where they reside, simplifying tasks such as backup or migration. It ensures predictability and stability — important attributes in volatile environments.

Real App Setup: WordPress Example

To show how Traefik integrates with real applications, here’s a Docker Compose example for a WordPress application:

services:
  db:
    image: mysql:8.0
    container_name: wordpress_db
    restart: unless-stopped
    command: --default-authentication-plugin=mysql_native_password
    environment:
      MYSQL_DATABASE: ${WORDPRESS_DB_NAME}
      MYSQL_USER: ${WORDPRESS_DB_USER}
      MYSQL_PASSWORD: ${WORDPRESS_DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    volumes:
      - /data/wordpress/db:/var/lib/mysql
    networks:
      - wordpress_internal
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s

  wordpress:
    image: wordpress:latest
    container_name: wordpress_app
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: ${WORDPRESS_DB_NAME}
      WORDPRESS_DB_USER: ${WORDPRESS_DB_USER}
      WORDPRESS_DB_PASSWORD: ${WORDPRESS_DB_PASSWORD}
    volumes:
      - /data/wordpress/html:/var/www/html
    networks:
      - wordpress_internal
      - traefik_proxy
    labels:
      - traefik.enable=true
      - traefik.docker.network=proxy
      - traefik.http.routers.example-http.rule=Host(`${DOMAIN}`)
      - traefik.http.routers.example-http.entrypoints=web
      - traefik.http.routers.example-http.middlewares=example-https-redirect
      - traefik.http.middlewares.example-https-redirect.redirectscheme.scheme=https
      - traefik.http.routers.example.rule=Host(`${DOMAIN}`)
      - traefik.http.routers.example.entrypoints=websecure
      - traefik.http.routers.example.tls.certresolver=leresolver
      - traefik.http.routers.example.tls=true
      - traefik.http.services.example.loadbalancer.server.port=80

networks:
  wordpress_internal:
    name: wordpress_internal
    driver: bridge
  traefik_proxy:
    external: true

A Caveat: Basic Auth Can Fail Silently

In setting up basic authentication for the Traefik dashboard, a common pitfall arises which can cause ample frustration. You might encounter a situation where password authentication fails despite appearing correct. This usually stems from Docker Compose’s handling of special characters in password hashes generated by commands like htpasswd.

Problem Explained

When you generate a hashed password and place it in your Docker Compose file (Line 63), the $ character within the hash often causes silent failures. Docker Compose interprets these as placeholders for environment variables, leading to corrupted hash inputs.

For example, after generating a hash like:

htpasswd -nb admin 'mypassword'

You might get an output such as:

admin:$apr1$abc123$XYZ...

However, employing this directly in your compose file without adjustments:

traefik.http.middlewares.traefik-auth.basicauth.users=admin:$apr1$abc123$XYZ...

Will result in Docker trying to interpret $apr1 and $abc123, breaking the authentication.

The Correct Approach

To address this, you must escape the dollar signs by doubling them:

traefik.http.middlewares.traefik-auth.basicauth.users=admin:$$apr1$$abc123$$XYZ...

This instructs Docker Compose to treat them as literal characters instead of variable markers. Understanding this quirks in syntax is crucial to ensuring your authentication setup works seamlessly.

Debugging Tip (Very Useful)

If you suspect any discrepancies in your configuration or observe unexpected behavior, a quick way to diagnose is with:

docker compose config

This command helps you visualize the resolved configurations Docker passes to containers, making it easier to spot misconfigurations or parsing errors. In the context of password issues, it’s insightful to check if specialized characters like $ are appropriately handled in the actual configurations.

Ready-to-Use Summary

For a summarized checklist, ensuring a solid baseline:

  • Keep all data centralized in /data/traefik.
  • Ensure services are not exposedByDefault.
  • Enforce HTTPS redirection at the entry point.
  • Mount Docker socket as read-only for security.
  • Escape $ in Basic Auth entries to avoid hidden pitfalls.

Adopting these practices provides a reliable foundation for deploying Traefik in production environments.


Enjoyed this article? Support my work with a coffee ☕ on Ko-fi.
Buy Me a Coffee at ko-fi.com
DigitalOcean Referral Badge
Sign up to get $200, 60-day account credit !