Introduction

Traefik is a cloud-native reverse proxy and load balancer that integrates with Docker, Kubernetes, and Consul for automatic service discovery. It handles Let's Encrypt certificates automatically and supports middleware for authentication, rate limiting, and headers. Ansible deploys Traefik as a system service or Docker container with full configuration management.

Deploy Traefik as System Service

---
- name: Deploy Traefik
  hosts: proxy_servers
  become: true
  vars:
    traefik_version: "3.1.0"
    traefik_config_dir: /etc/traefik
    traefik_acme_email: admin@example.com
    traefik_dashboard_enabled: true
    traefik_dashboard_users:
      - "admin:{{ vault_traefik_dashboard_password | password_hash('bcrypt') }}"
  tasks:
    - name: Create Traefik user
      ansible.builtin.user:
        name: traefik
        system: true
        shell: /usr/sbin/nologin

    - name: Download Traefik
      ansible.builtin.get_url:
        url: "https://github.com/traefik/traefik/releases/download/v{{ traefik_version }}/traefik_v{{ traefik_version }}_linux_amd64.tar.gz"
        dest: /tmp/traefik.tar.gz

    - name: Extract Traefik
      ansible.builtin.unarchive:
        src: /tmp/traefik.tar.gz
        dest: /usr/local/bin/
        remote_src: true
        mode: '0755'
        include: [traefik]

    - name: Create directories
      ansible.builtin.file:
        path: "{{ item }}"
        state: directory
        owner: traefik
        mode: '0750'
      loop:
        - "{{ traefik_config_dir }}"
        - "{{ traefik_config_dir }}/dynamic"
        - /var/lib/traefik

    - name: Deploy static config
      ansible.builtin.template:
        src: traefik.yml.j2
        dest: "{{ traefik_config_dir }}/traefik.yml"
        owner: traefik
        mode: '0640'
      notify: restart traefik

    - name: Deploy dynamic config
      ansible.builtin.template:
        src: dynamic.yml.j2
        dest: "{{ traefik_config_dir }}/dynamic/config.yml"
        owner: traefik
        mode: '0640'

    - name: Allow bind to ports 80/443
      ansible.builtin.command: setcap 'cap_net_bind_service=+ep' /usr/local/bin/traefik
      changed_when: true

    - name: Create systemd service
      ansible.builtin.copy:
        dest: /etc/systemd/system/traefik.service
        content: |
          [Unit]
          Description=Traefik
          After=network-online.target
          [Service]
          User=traefik
          ExecStart=/usr/local/bin/traefik --configFile={{ traefik_config_dir }}/traefik.yml
          Restart=on-failure
          RestartSec=5
          LimitNOFILE=65536
          [Install]
          WantedBy=multi-user.target
        mode: '0644'
      notify:
        - daemon reload
        - restart traefik

    - name: Start Traefik
      ansible.builtin.service:
        name: traefik
        state: started
        enabled: true

  handlers:
    - name: daemon reload
      ansible.builtin.systemd:
        daemon_reload: true
    - name: restart traefik
      ansible.builtin.service:
        name: traefik
        state: restarted

Static Config

# templates/traefik.yml.j2
api:
{% if traefik_dashboard_enabled %}
  dashboard: true
  insecure: false
{% endif %}

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

certificatesResolvers:
  letsencrypt:
    acme:
      email: {{ traefik_acme_email }}
      storage: /var/lib/traefik/acme.json
      httpChallenge:
        entryPoint: web

providers:
  file:
    directory: {{ traefik_config_dir }}/dynamic
    watch: true

{% if traefik_docker_enabled | default(false) %}
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
{% endif %}

log:
  level: INFO

accessLog:
  filePath: /var/log/traefik/access.log

metrics:
  prometheus:
    entryPoint: websecure

Dynamic Config

# templates/dynamic.yml.j2
http:
  routers:
{% if traefik_dashboard_enabled %}
    dashboard:
      rule: "Host(`traefik.{{ traefik_domain }}`)"
      service: api@internal
      middlewares:
        - auth
      tls:
        certResolver: letsencrypt
{% endif %}

{% for service in traefik_services | default([]) %}
    {{ service.name }}:
      rule: "Host(`{{ service.domain }}`)"
      service: {{ service.name }}
      tls:
        certResolver: letsencrypt
{% if service.middlewares is defined %}
      middlewares:
{% for mw in service.middlewares %}
        - {{ mw }}
{% endfor %}
{% endif %}
{% endfor %}

  services:
{% for service in traefik_services | default([]) %}
    {{ service.name }}:
      loadBalancer:
        servers:
{% for server in service.backends %}
          - url: "{{ server }}"
{% endfor %}
{% if service.health_check is defined %}
        healthCheck:
          path: {{ service.health_check }}
          interval: "10s"
{% endif %}
{% endfor %}

  middlewares:
    auth:
      basicAuth:
        users:
{% for user in traefik_dashboard_users %}
          - "{{ user }}"
{% endfor %}

    rate-limit:
      rateLimit:
        average: 100
        burst: 200

    security-headers:
      headers:
        stsSeconds: 63072000
        stsIncludeSubdomains: true
        stsPreload: true
        forceSTSHeader: true
        contentTypeNosniff: true
        browserXssFilter: true
        frameDeny: true

Service Variables

traefik_domain: example.com
traefik_services:
  - name: webapp
    domain: app.example.com
    backends:
      - "http://10.0.1.10:8080"
      - "http://10.0.1.11:8080"
    health_check: /health
    middlewares: [rate-limit, security-headers]
  - name: api
    domain: api.example.com
    backends:
      - "http://10.0.1.20:3000"
    health_check: /healthz
    middlewares: [rate-limit]

Traefik with Docker

- name: Deploy Traefik with Docker provider
  community.docker.docker_container:
    name: traefik
    image: "traefik:v{{ traefik_version }}"
    restart_policy: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /etc/traefik:/etc/traefik:ro
      - /var/lib/traefik:/var/lib/traefik
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedByDefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.letsencrypt.acme.email={{ traefik_acme_email }}"
      - "--certificatesresolvers.letsencrypt.acme.storage=/var/lib/traefik/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    labels:
      traefik.enable: "true"
      traefik.http.routers.dashboard.rule: "Host(`traefik.{{ traefik_domain }}`)"
      traefik.http.routers.dashboard.service: "api@internal"

Health Check

- name: Check Traefik health
  ansible.builtin.uri:
    url: "http://localhost:8080/ping"
    status_code: 200

- name: Check Traefik API
  ansible.builtin.uri:
    url: "http://localhost:8080/api/http/routers"
    return_content: true
  register: routers

Troubleshooting

Certificate Not Issuing

- name: Check ACME storage
  ansible.builtin.command: cat /var/lib/traefik/acme.json
  register: acme_data
  changed_when: false
  no_log: true

Check Traefik Logs

- name: View recent logs
  ansible.builtin.command: journalctl -u traefik --no-pager -n 100
  register: traefik_logs
  changed_when: false

Conclusion

Traefik is the cloud-native alternative to Nginx/HAProxy — it discovers services automatically from Docker labels or file-based config, handles TLS certificates via Let's Encrypt without any cron jobs, and provides middleware for security headers, rate limiting, and authentication. Ansible templates both static and dynamic configs, deploys as a system service or Docker container, and manages the full proxy lifecycle as code.