Traefik Proxy Guide: Configuring public Domain Names for Docker Containers
Disclaimer
This article introduces Traefik, a modern reverse proxy and load balancer for deploying micro-services. We will cover its basics, key features, configuration, and integration with Docker. By the end, you should understand how to set up and use Traefik in your projects.
The final example in this article builds on concepts discussed in my previous article: How to Automate Deployment with Ansible and GitHub Actions. In that article, I explained how to streamline your deployment process using Ansible for configuration management and GitHub Actions for continuous integration and deployment. If you haven't read it yet, I highly recommend doing so to fully grasp the advanced section of this article. Understanding the automation techniques covered there will be crucial for implementing the final example effectively.
Introduction
Traefik is a modern open-source reverse proxy and load balancer. It’s built with simplicity and flexibility in mind. Traefik excels in a containerized setup, especially with micro-services.
(Image source: Traefik Documentation)
Key features of Traefik proxy
Traefik comes with amazing features, including:
Dynamic configurations
Automatic SSL/TLS
Load balancing
Middleware support
Integration with other platforms
Configuring Traefik
Traefik configurations are written in familiar syntax i.e TOML or YAML .
Traefik gives you flexibility on the type of configuration you are comfortable with. It support the following types of configurations;
Static configuration where every or some configurations are defined in a single file;
traefik.yml
Dynamic configuration where different providers are supported and used to automatically detect or create Traefik configurations.
For example, with the Docker provider, configurations are defined as labels on Docker containers and automatically detected as Traefik configurations.
Other supported providers include Swarm, Nomad, Kubernetes, Consul, and many more.
Traefik & Docker
In this article, our focus is on Traefik’s Docker provider. We are going to install traefik as a docker container and the use it to proxy a python flask application ( also a docker container ).
A Demo project
For this first demo, I’ll explain a simple setup using docker-compose. In the next demo, we will integrate with GitHub Actions and Ansible.
Here is the project structure.
traefik-demo\
|-- docker-compose.yml
|-- traefik\
| |-- certs\
| |-- traefik.yml
To get started, we need to define a Traefik configuration file. Here is what it should look like:
traefik.yml
global:
checkNewVersion: true
sendAnonymousUsage: false # true by default
# (Optional) Log information
# ---
# log:
# level: ERROR # DEBUG, INFO, WARNING, ERROR, CRITICAL
# format: common # common, json, logfmt
# filePath: /var/log/traefik/traefik.log
# (Optional) Accesslog
# ---
# accesslog:
# format: common # common, json, logfmt
# filePath: /var/log/traefik/access.log
# (Optional) Enable API and Dashboard
# ---
api:
dashboard: true # true by default
insecure: false # Don't do this in production!
# Entry Points configuration
# ---
entryPoints:
web:
address: :80
# (Optional) Redirect to HTTPS
# ---
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: :443
# Configure your CertificateResolver here...
# ---
certificatesResolvers:
staging:
acme:
email: "{ EMAIL }"
storage: /etc/traefik/certs/acme.json
caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
tlsChallenge: {}
httpChallenge:
entryPoint: web
production:
acme:
email: "{ EMAIL }"
storage: /etc/traefik/certs/acme.json
caServer: "https://acme-v02.api.letsencrypt.org/directory"
tlsChallenge: {}
httpChallenge:
entryPoint: web
providers:
docker:
exposedByDefault: false # Default is true
file:
# watch for dynamic configuration changes
directory: /etc/traefik
watch: true
For automatic SSL to work, make sure to add a valid email. Replace “{ EMAIL }” with your own email address.
Let’s see how the docker compose file would look like.
docker-compose.yml
# Run traefik webserver, portainer, watchtower
services:
traefik:
container_name: "traefik"
image: "traefik:v2.5"
ports:
- "80:80"
- "443:443"
# - "59808:8080" # Uncomment this to expose the traefik dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik-ssl-certs:/ssl-certs
- ./traefik:/etc/traefik
networks:
- traefik_network
restart: always
api:
image: ghcr.io/jackkweyunga/auto-deploy-flask-to-server:main
environment:
- FLASK_ENV=production
- DEBUG=${DEBUG}
expose:
- "5000"
network:
- traefik_network
labels:
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`mydomain.com`)
- traefik.http.routers.api.entrypoints=websecure
- traefik.http.services.api.loadbalancer.server.port=5000
- traefik.http.routers.api.tls.certresolver=production
restart: always
networks:
traefik_network:
external: true
volumes:
traefik-ssl-certs:
Notice that we have defined a traefik_network
in the docker-compose file as external. We need to create this network before running the file. Every other Docker container that will be proxied by Traefik must join the traefik_network
.
sudo docker network create traefik_network
Notice the labels we added to the API service and that it is also part of the traefik_network
. The labels define Traefik configurations. Below is a detailed explanation of each label.
traefik.enable=true
Tells Traefik to proxy this container/service.
traefik.http.routers.api.rule=Host(
mydomain.com
)Tells Traefik to route all traffic from mydomain.com to this container/service. Ensure proper DNS records are set up to point the domain name to the server running Traefik.
traefik.http.routers.api.entrypoints=websecure
Tells Traefik to use a secure entry point for this container/service, as defined in the traefik.yml configuration file. This means using port 443, the secure port.
traefik.http.services.api.loadbalancer.server.port=5000
Tells Traefik to direct traffic to port 5000 of the container.
traefik.http.routers.api.tls.certresolver=production
Tells Traefik to use the production Let's Encrypt endpoints when fetching SSL certificates. This is also referenced in the traefik.yml configuration file.
NOTE: For each service, the router name must be unique from router names in other services. The same rule applies to service names. The router name is the part that comes after the dot, like traefik.http.routers.{router name}. If router names of different services are the same, proxying will fail.
When the setup runs on the server, Traefik will listen on ports 80 and 443. Any traffic on port 80 will be redirected to the secure port 443. Also, when someone visits mydomain.com
, a response from the Flask application will be returned.
Add traefik to your CI/CD workflow
Referring to the article How to Automate Deployment with Ansible and GitHub Actions, where we set up our CI/CD workflow with Ansible and GitHub Actions, we will now build on that and add the Traefik proxy. This will let us assign public domain names to our services.
Follow this GitHub repository: https://github.com/jackkweyunga/auto-deploy-flask-to-server
Traefik Ansible role
First, we add a Traefik Ansible role. In this role, we define the automation needed to configure and run Traefik on a remote server. Below is the file structure, which can be added to the structure mentioned in the previous article.
--- previous ---
.github\
workflows\
deploy-proxy.yml
ansible\
proxy.yml
traefik\
tasks\
main.yml
templates\
traefik.yml.jinja2
templates/traefik.yml.jinja2
global:
checkNewVersion: true
sendAnonymousUsage: false # true by default
# (Optional) Log information
# ---
# log:
# level: ERROR # DEBUG, INFO, WARNING, ERROR, CRITICAL
# format: common # common, json, logfmt
# filePath: /var/log/traefik/traefik.log
# (Optional) Accesslog
# ---
# accesslog:
# format: common # common, json, logfmt
# filePath: /var/log/traefik/access.log
# (Optional) Enable API and Dashboard
# ---
api:
dashboard: true # true by default
insecure: false # Don't do this in production!
# Entry Points configuration
# ---
entryPoints:
web:
address: :80
# (Optional) Redirect to HTTPS
# ---
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: :443
# Configure your CertificateResolver here...
# ---
certificatesResolvers:
staging:
acme:
email: {{ EMAIL }}
storage: /etc/traefik/certs/acme.json
caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
tlsChallenge: {}
httpChallenge:
entryPoint: web
production:
acme:
email: {{ EMAIL }}
storage: /etc/traefik/certs/acme.json
caServer: "https://acme-v02.api.letsencrypt.org/directory"
tlsChallenge: {}
httpChallenge:
entryPoint: web
serversTransport:
insecureSkipVerify: true
providers:
docker:
exposedByDefault: false # Default is true
file:
# watch for dynamic configuration changes
directory: /etc/traefik
watch: true
This Traefik configuration template allows us to pass variables to it while saving it on the remote server. In this case, an EMAIL will be passed.
tasks/main.yml
---
- name: Preparing required files and Directories in /etc/traefik
become: true
block:
- name: Create directory
file:
path: /etc/traefik
state: directory
- name: Create directory2
file:
path: /etc/traefik/certs
state: directory
- name: Copy config file
ansible.builtin.template:
src: templates/traefik.yml.jinja2
dest: /etc/traefik/traefik.yaml
- name: Configuring traefik
become: true
block:
- name: Create ssl-certs Volume
community.docker.docker_volume:
name: traefik-ssl-certs
register: v_output
ignore_errors: true
- name: Debug output
ansible.builtin.debug:
var: v_output
- block:
- name: Create traefik_network
become: true
community.docker.docker_network:
name: traefik_network
register: n_output
ignore_errors: true
- name: Debug output
ansible.builtin.debug:
var: n_output
when: v_output
- block:
- name: Deploy Traefik
community.docker.docker_container:
name: traefik
image: "traefik:v2.10"
ports:
- "80:80"
- "443:443"
- "59808:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- traefik-ssl-certs:/ssl-certs
- /etc/traefik:/etc/traefik
networks:
- name: "traefik_network" # required. Name of the network to operate on.
restart_policy: always
labels:
com.centurylinklabs.watchtower.enable: "false"
register: d_output
ignore_errors: true
- name: Debug output
ansible.builtin.debug:
var: d_output
when: n_output
In this task, where the magic happens, we create the required folders, files, Docker volumes, and Docker networks (traefik_network). Then, we use Ansible’s docker_container community plugin to run Traefik on the remote server(s). If you look closely, you'll notice the similarity to the docker-compose.yml we used in the first demo.
ansible/proxy.yml
---
- hosts: webservers
vars_files:
- secret
roles:
- traefik
vars:
EMAIL: "{{ lookup('ansible.builtin.env', 'EMAIL') }}"
This is the main Ansible playbook we’ll run to configure Traefik. Notice how it references the Traefik role. We also read the EMAIL variable from the environment variable.
Now that the Ansible configurations are ready, let's add a GitHub workflow to run Ansible on demand with GitHub Action runners.
Add the proxy GitHub Workflow
.github/workflows/deploy-proxy.yml
name: proxy
on:
workflow_dispatch:
inputs:
REMOTE_USER:
type: string
description: 'Remote User'
required: true
default: 'ubuntu' # Edit here
EMAIL:
type: string
description: 'Email for fetching certs'
required: true
default: '<put a valid email here>' # Edit here
HOME_DIR:
type: string
description: 'Home Directory'
required: true
default: '/home/ubuntu' # Edit here
TARGET_HOST:
description: 'Target Host'
required: true
default: "< ip / domain >" # Edit here
jobs:
ansible:
runs-on: ubuntu-latest
env:
EMAIL: "${{ inputs.EMAIL }}"
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Add SSH Keys
run: |
cat << EOF > ansible/devops-key
${{ secrets.SSH_DEVOPS_KEY_PRIVATE }}
EOF
- name: Update devops private key permissions
run: |
chmod 400 ansible/devops-key
- name: Install Ansible
run: |
pip install ansible
- name: Adding or Override Ansible inventory File
run: |
cat << EOF > ansible/inventory.ini
[webservers]
${{ inputs.TARGET_HOST }}
EOF
- name: Adding or Override Ansible Config File
run: |
cat << EOF > ./ansible/ansible.cfg
[defaults]
ansible_python_interpreter='/usr/bin/python3'
deprecation_warnings=False
inventory=./inventory.ini
remote_tmp="${{ inputs.HOME_DIR }}/.ansible/tmp"
remote_user="${{ inputs.REMOTE_USER }}"
host_key_checking=False
private_key_file = ./devops-key
retries=2
EOF
- name: Run main playbook
run: |
sh ansible/create-sudo-password-ansible-secret.sh ${{ secrets.SUDO_PASSWORD }}
ANSIBLE_CONFIG=ansible/ansible.cfg ansible-playbook ansible/proxy.yml --vault-password-file=ansible/vault.txt
Make sure to edit the EMAIL and TARGET_HOST inputs.
We can now edit the flask-api ansible role to include traefik configurations via container labels.
Update the flask-api Ansible role
Edit ansible/flask-api/files/docker-compose.yml
volumes:
portainer-data:
# Added networks
networks:
traefik_network:
external: true
services:
portainer:
image: portainer/portainer-ce:alpine
container_name: portainer
command: -H unix:///var/run/docker.sock
expose:
- "9000"
volumes:
# Connect docker socket to portainer
- "/var/run/docker.sock:/var/run/docker.sock"
# Persist portainer data
- "portainer_data:/data"
restart: always
# Added networks
networks:
- traefik_network
# Added lables
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`${PORTAINER_DOMAIN}`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=production"
- "traefik.http.routers.portainer.tls=true"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
- "traefik.docker.network=traefik_network"
watchtower:
container_name: "watchtower"
image: "docker.io/containrrr/watchtower"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# To enable docker authentication, uncomment the line below.
# You also need to make sure you are logged in to docker in the server
# E.g by running: sudo docker login ghcr.io
# - /root/.docker/config.json:/config.json:ro
restart: always
environment:
TZ: Africa/Dar_es_Salaam
WATCHTOWER_LIFECYCLE_HOOKS: "1" # Enable pre/post-update scripts
command: --debug --cleanup --interval 30
web:
image: ghcr.io/jackkweyunga/auto-deploy-flask-to-server:main
environment:
- FLASK_ENV=production
- DEBUG=${DEBUG}
expose:
- "5000"
restart: always
# Added networks
networks:
- traefik_network
# Added labels
labels:
- com.centurylinklabs.watchtower.enable=true
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`${FLASK_API_DOMAIN}`)
- traefik.http.routers.api.entrypoints=websecure
- traefik.http.services.api.loadbalancer.server.port=5000
- traefik.http.routers.api.tls.certresolver=production
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
memory: "256m"
cpus: "0.50"
Check all places with the comment #Added ... These sections are due to adding the Traefik proxy configurations.
Notice new variables:
FLASK_API_DOMAIN: The domain name of our Flask API, e.g., api.mydomain.com
PORTAINER_DOMAIN: The domain name of our Portainer instance, e.g., portainer.mydomain.com
As a result, we need to add these variables to ansible/deploy.yml
so that we can read them from the environment in GitHub Actions runners.
Edit: ansible/deploy.yml
---
- hosts: webservers
# an encrypted ansible secret file containing the sudo password
vars_files:
- secret
roles:
- services
environment:
DEBUG: "{{ lookup('ansible.builtin.env', 'DEBUG') }}"
# Added new variables
FLASK_API_DOMAIN: "{{ lookup('ansible.builtin.env', 'FLASK_API_DOMAIN') }}"
PORTAINER_DOMAIN: "{{ lookup('ansible.builtin.env', 'PORTAINER_DOMAIN') }}"
Update the GitHub workflow for deployment
Finally, we edit the deploy.yml
GitHub actions workflow file to include the domains
name: ansible-deploy
on:
workflow_dispatch:
inputs:
REMOTE_USER:
type: string
description: 'Remote User'
required: true
default: 'ubuntu'
HOME_DIR:
type: string
description: 'Home Directory'
required: true
default: '/home/ubuntu'
TARGET_HOST:
description: 'Target Host'
required: true
default: "example.com" # Change this to your server IP or Domain
jobs:
ansible:
runs-on: ubuntu-latest
env:
DEBUG: 0
# Added new variables
FLASK_API_DOMAIN: "api.mydomain.com" # Add your domain
PORTAINER_DOMAIN: "portainer.mydomain.com" # Add your domain
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Add SSH Keys
run: |
cat << EOF > ansible/devops-key
${{ secrets.SSH_DEVOPS_KEY_PRIVATE }}
EOF
- name: Update devops private key permissions
run: |
chmod 400 ansible/devops-key
- name: Install Ansible
run: |
pip install ansible
- name: Adding or Override Ansible inventory File
run: |
cat << EOF > ansible/inventory.ini
[webservers]
${{ inputs.TARGET_HOST }}
EOF
- name: Adding or Override Ansible Config File
run: |
cat << EOF > ./ansible/ansible.cfg
[defaults]
ansible_python_interpreter='/usr/bin/python3'
deprecation_warnings=False
inventory=./inventory.ini
remote_tmp="${{ inputs.HOME_DIR }}/.ansible/tmp"
remote_user="${{ inputs.REMOTE_USER }}"
host_key_checking=False
private_key_file = ./devops-key
retries=2
EOF
- name: Run deploy playbook
run: |
sh ansible/create-sudo-password-ansible-secret.sh ${{ secrets.SUDO_PASSWORD }}
ANSIBLE_CONFIG=ansible/ansible.cfg ansible-playbook ansible/deploy.yml --vault-password-file=ansible/vault.txt
Make sure to add working domain, that have DNS records directing them to the target server.
Operation
To install traefik on a remote server, run the proxy
GitHub workflow. After that run the deploy
GitHub workflow to update your infrastructure with the newly added traefik configurations and Domain names.
After both runs are successful, you can now visit your domains securely (with SSL). If SSL has not activated yet, give it some time and monitor Traefik’s logs in Portainer in case there is an issue.
Happy Configuring !
Conclusion
If you have reached this point, it is evident that Traefik significantly enhances your workflow. The Traefik proxy is an outstanding project that integrates seamlessly with containers and can be easily incorporated into your CI/CD pipeline.
With the addition of Traefik, our setup is nearly complete. Stay tuned for my next article.
Seeking expert guidance in Ops, DevOps, or DevSecOps? I provide customized consultancy services for personal projects, small teams, and organizations. Whether you require assistance in optimizing operations, improving your CI/CD pipelines, or implementing strong security practices, I am here to support you. Let's collaborate to elevate your projects. Contact me today | LinkedIn | GitHub
Subscribe to my newsletter
Read articles from jack kweyunga directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
jack kweyunga
jack kweyunga
Am a DevSecOps practitioner, software engineer and a life long learner.