Traefik — обратный прокси и балансировщик нагрузки, написанный на Go. Его главная особенность в том, что маршруты он собирает не из одного большого конфига, а из данных провайдеров: Docker, Podman, Kubernetes, файлов YAML. Добавили контейнер с метками — Traefik сам создал роутер и сервис, перечитал состояние и начал проксировать трафик. Для домашней лаборатории и небольшого продакшена это экономит много времени, но требует понимания модели объектов и дисциплины в диагностике. Ниже — рабочий сценарий для Docker с проверкой каждого шага.
Как устроена модель Traefik
В основе лежат четыре сущности. Entrypoint — порт, который слушает прокси, например 80 и 443. Router — правило вида Host(`app.example.com`), которое решает, куда отправить запрос. Service — адрес и порт бэкенда, куда проксирует роутер. Middleware — промежуточный слой: редирект, заголовки, базовая авторизация, ограничение скорости. Провайдер Docker читает метки контейнеров и превращает их в эти объекты; файловый провайдер делает то же самое из YAML.
- Маршруты обновляются без перезапуска процесса.
- Один контейнер может обслуживать десятки доменов.
- Сертификаты Let’s Encrypt выпускаются автоматически.
- Конфигурация хранится рядом с приложением, а не в отдельном файле.
Быстрый запуск в Docker Compose
Начните с отдельной сети: к ней будут подключаться прокси и приложения. Сокет Docker монтируется только для чтения — этого достаточно для обнаружения контейнеров. Каталог для сертификатов лучше вынести на хост, чтобы acme.json пережил пересоздание контейнера.
services:
traefik:
image: traefik:v3.3
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.le.acme.email=admin@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=web
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks: [proxy]
networks:
proxy:
name: proxy
Запуск и проверка состояния выполняются обычными командами Compose. Если контейнер падает, смотрите не только статус, но и полный лог: Traefik часто пишет причину отказа от сертификата или ошибку разбора метки.
docker compose up -d
docker compose ps
docker compose logs -f --tail=100 traefik
curl -I http://localhost
Маршрутизация через метки
Приложение не обязано публиковать порт на хост. Достаточно подключить его к сети proxy и описать правила. Параметр exposedbydefault=false важен: без него любой контейнер с портом попадёт в маршрутизацию, что создаёт лишнюю поверхность атаки.
services:
app:
image: nginx:alpine
networks: [proxy]
labels:
- traefik.enable=true
- traefik.http.routers.app.rule=Host(`app.example.com`)
- traefik.http.routers.app.entrypoints=websecure
- traefik.http.routers.app.tls.certresolver=le
- traefik.http.services.app.loadbalancer.server.port=80
- traefik.docker.network=proxy
Обратите внимание на имя сервиса: traefik.http.services.app.... Оно должно совпадать с именем в ссылке роутера, иначе Traefik создаст сервис с другим именем и вернёт 502. Внутренний порт указывается тот, который слушает контейнер, а не опубликованный на хосте.
HTTPS, редирект и защита
HTTP-роутер нужен только для редиректа и прохождения ACME-проверки. Не закрывайте 80-й порт полностью: HTTP-01 challenge должен быть доступен извне. Для служебных панелей добавляйте базовую авторизацию и заголовки безопасности.
labels:
- traefik.http.middlewares.redirect.redirectscheme.scheme=https
- traefik.http.routers.app-http.rule=Host(`app.example.com`)
- traefik.http.routers.app-http.entrypoints=web
- traefik.http.routers.app-http.middlewares=redirect
- traefik.http.middlewares.sec.headers.stsSeconds=31536000
- traefik.http.middlewares.sec.headers.contentTypeNosniff=true
- traefik.http.middlewares.sec.headers.frameDeny=true
- traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$hash
Хеш для basic auth получают через htpasswd -nb admin пароль. Символы доллара в Compose нужно удваивать, иначе переменная подставится как пустая строка. HSTS включайте только после того, как убедились в работоспособности HTTPS: браузер запомнит заголовок надолго.
Файловый провайдер и горячая перезагрузка
Не всё живёт в контейнерах: старый сервис на виртуальной машине или демон на самом хосте тоже нужно опубликовать. Для этого подключают файловый провайдер и описывают роутеры в отдельном YAML. Режим watch перечитывает каталог при изменении, поэтому перезапуск контейнера не требуется. При синтаксической ошибке Traefik сохраняет последнюю рабочую конфигурацию и пишет причину в лог — это лучше, чем молча потерять маршрут.
providers:
file:
directory: /etc/traefik/dynamic
watch: true
http:
routers:
legacy:
rule: "Host(`legacy.example.com`)"
service: legacy
services:
legacy:
loadBalancer:
servers:
- url: "http://192.168.1.20:8080"
Файлы удобно держать в git и проверять локально тем же образом, что и в продакшене. Не смешивайте в одном каталоге временные файлы редактора: провайдер попытается разобрать их как конфигурацию и напишет ошибку.
Панель управления и API
Дашборд удобен для отладки, но в интернет его выставлять нельзя. Включайте API, закрывайте отдельным роутером с авторизацией и разрешайте доступ по IP или через локальную сеть. Сырые данные API показывают все роутеры, сервисы и middleware — это лучший способ понять, почему запрос ушёл не туда.
command:
- --api.dashboard=true
- --api.insecure=false
curl -s http://127.0.0.1:8080/api/rawdata | jq '.routers'
curl -s http://127.0.0.1:8080/api/overview | jq
Диагностика типовых сбоев
Код 404 означает, что ни один роутер не совпал с запросом: проверьте Host, entrypoint и то, что контейнер действительно в сети proxy. Код 502 говорит, что роутер есть, а сервис недоступен: обычно неверный внутренний порт или приложение слушает только localhost. Ошибки сертификата почти всегда связаны с DNS: имя должно указывать на внешний IP, а 80-й порт — быть открыт.
docker inspect $(docker compose ps -q app) --format '{{json .NetworkSettings.Networks}}'
curl -v --resolve app.example.com:443:127.0.0.1 https://app.example.com
openssl s_client -connect app.example.com:443 -servername app.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates
docker run --rm --network proxy curlimages/curl -s http://app:80/ | head
Чек-лист перед продакшеном
- Тег образа зафиксирован, а не равен latest.
- Сокет Docker смонтирован read-only; в идеале — через ограничивающий прокси.
- Каталог acme.json имеет права 600 и входит в резервное копирование.
- Включены access-логи и метрики, настроен внешний мониторинг.
- Дашборд не доступен публично без авторизации.
- Проверены редирект, HSTS, лимиты и поведение при недоступном бэкенде.
- Отдельно проверено поведение при медленном DNS и истёкшем сертификате.
Итог
Traefik выигрывает там, где контейнеры появляются и исчезают часто: маршруты живут рядом с приложением, а сертификаты обновляются сами. Начните с минимального Compose-файла, проверьте один домен, затем добавляйте middleware и мониторинг. Держите конфигурацию в git, документируйте метки и не открывайте API наружу — и автоконфигурация останется удобством, а не источником неожиданностей.