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.