Traefik v3: настройка пяти приложений в Docker Compose
Разверните пять сервисов на одном IP с Traefik v3. Статья описывает настройку правил Host, автоматизацию TLS через Let's Encrypt и решение ошибки acme.json, блокирующей запуск.
Один IP, пять приложений, один порт 443
Ваш VPS имеет один публичный IPv4-адрес и один TCP-порт 443. Вы хотите разместить на нем Gitea, тестовую копию приложения, внутреннюю панель управления, страницу статуса и обработчик вебхуков — пять доменных имен на одном сервере. Reverse proxy — это процесс, который занимает порты :80 и :443, считывает заголовок Host в каждом запросе и передает его нужному контейнеру. Traefik выполняет эту задачу, а также получает и обновляет сертификаты для каждого доменного имени без необходимости вручную запускать certbot. Nginx и Caddy справятся с этими пятью именами не хуже, поэтому, если выбор еще не сделан, стоит сравнить эти три прокси по способу обработки сертификатов и трудозатратам на настройку каждого приложения, прежде чем привязывать всё к одному из них.
Главное отличие Traefik от блока server {} в Nginx заключается в источнике конфигурации. В Nginx вы редактируете файл и перезагружаете сервис, а жизненный цикл сертификатов остается отдельной задачей — это рабочий процесс, который вы используете, когда выпускаете сертификаты Let's Encrypt через certbot для Nginx, где таймер обновления существует отдельно от веб-сервера. Docker-провайдер Traefik отслеживает поток событий Docker и считывает метки (labels) ваших контейнеров: запустите контейнер с меткой правила Host(), и он станет доступен для маршрутизации в течение секунды; остановите его, и маршрут исчезнет. В этом же кроется и подвох. Конфигурация, распределенная по меткам, находится в пяти местах одновременно, а при ошибке в метке никакой ошибки не выводится: контейнер просто не попадает в маршрутизацию, а Traefik не выдает никаких уведомлений.
Четыре базовых понятия
- Entrypoints — это слушающие сокеты. Вы определите два:
webна:80иwebsecureна:443. - Routers — сопоставляют запрос (
Host(...)) и направляют его к сервису. Сертификаты запрашиваются для каждого роутера черезtls.certresolver. - Services — это backend, контейнер и порт, который он слушает внутри сети Docker.
- Middlewares — располагаются между роутером и сервисом: базовая аутентификация, списки разрешенных IP-адресов, перезапись заголовков, редиректы.
Эти четыре понятия — названия Traefik для задач, которые в противном случае вы выполняли бы вручную: роутер — это server_name, сервис — это целевой proxy_pass, а middleware — это директивы заголовков и аутентификации, которые вы задаете самостоятельно, когда создаете блок конфигурации сервера nginx построчно.
Статическая конфигурация (entrypoints, провайдеры, ACME) передается через командную строку Traefik или в traefik.yml, и её изменение требует перезапуска Traefik. Динамическая конфигурация (роутеры, сервисы, middlewares) поступает из меток контейнеров и обновляется «на лету» (hot-reload). Путаница между ними — частая причина того, почему «флаг не работает».
Файл compose
Общая сеть Docker с именем proxy является основой. Traefik получает доступ к контейнеру, только если они оба находятся в этой сети.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.5
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000docker compose up -d, затем docker compose logs -f traefik. Каждое дополнительное приложение — это копия блока gitea с собственным именем маршрутизатора, собственным Host() и собственным внутренним портом. Установка Nextcloud в Docker с TLS и резервным копированием подключается аналогичным образом: удалите опубликованные порты, добавьте контейнер в сеть proxy и позвольте меткам маршрутизатора настроить имя хоста и сертификат.
Пять деталей здесь особенно важны.
exposedByDefault=false делает контейнер невидимым для Traefik, пока у него нет метки traefik.enable=true. Если её не указать, для каждого запущенного вами контейнера, включая временный postgres, созданный для проверки, будет сгенерирован маршрут.
providers.docker.network=proxy указывает Traefik, какую сеть использовать, если контейнер подключен к нескольким. Если пропустить этот параметр, Traefik может выбрать неверный IP-адрес контейнера, что приведет к ошибке 502, которая выглядит как сбой приложения.
loadbalancer.server.port=3000 — это порт внутри контейнера; Gitea слушает на 3000 именно там. Обратите внимание, что ни один контейнер приложения не публикует порты, это делает только Traefik.
Перенаправление в точке входа web преобразует запросы в открытом виде в 308 на HTTPS. Порт 80 остается открытым в любом случае: он нужен для ACME HTTP challenge, а также для пользователей, которые вводят имя хоста без указания протокола.
Удвоенный $$ в хеше basic-auth — это экранирование в Compose, а не опечатка. Сгенерируйте его с помощью htpasswd -nbB admin 'your-password' (пакет apache2-utils), а затем удвойте каждый $.
Сертификат и ловушка acme.json
tlschallenge=true выбирает TLS-ALPN-01: Let's Encrypt подключается к вашему серверу по порту 443, а Traefik отвечает на запрос в рамках TLS-рукопожатия. Альтернатива — HTTP-01, использующий порт 80. Замените строку tlschallenge в списке command: Traefik на следующие две:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webОба варианта рабочие. Для обоих необходимо, чтобы публичная DNS-запись для имени хоста уже указывала на ваш VPS, так как центр сертификации разрешает имя и подключается извне. Сначала создайте A (и AAAA) запись, проверьте её с помощью dig +short git.example.com, затем запускайте Traefik.
Теперь о ловушке, на которую многие тратят целый вечер. Traefik хранит ключ учетной записи ACME и все выданные сертификаты в одном файле acme.json. Если этот файл доступен для чтения группе или всем пользователям, Traefik выводит сообщение, похожее на следующее, и останавливается:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600Правильное решение — то, что приведено выше: примонтируйте директорию и позвольте Traefik самому создать файл с нужными правами доступа. Если вы создали acme.json с помощью touch, ваш umask установил права 644. Исправьте их на хосте:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikДелайте резервные копии этой директории вместе с томами ваших приложений. Потеря данных не критична, сертификаты можно перевыпустить, но одновременный перевыпуск для пяти имен хостов приведет к срабатыванию лимитов по количеству запросов.
Используйте staging CA во время отладки. Раскомментируйте строку caserver, добейтесь корректной работы всех маршрутов, затем закомментируйте её и удалите acme.json, чтобы запросить «боевые» сертификаты. Let's Encrypt разрешает пять дублирующих сертификатов в неделю для одного и того же набора имен хостов и ограничивает количество повторных неудачных проверок для одного имени. Staging выдает недоверенные сертификаты, браузер будет выдавать предупреждение — это сигнал того, что всё работает, при этом лимиты там значительно мягче.
Панель управления — это инструмент администрирования, а не демонстрационная версия
Большинство руководств по быстрому старту предлагают установить --api.insecure=true, что открывает доступ к панели управления на порту 8080 без какой-либо аутентификации. На сервере с публичным IP-адресом это раскрывает топологию вашей сети, имена хостов, названия middleware и порты бэкендов любому, кто просканирует ваш узел.
Метки в сервисе traefik, приведенном выше, предлагают альтернативный подход: панель управления маршрутизируется как любое другое приложение, доступна по реальному доменному имени, защищена TLS и находится за basicauth. service=api@internal — это механизм, который связывает маршрутизатор со встроенным API Traefik. Усильте защиту, добавив цепочку IP allow-list, которая применяется слева направо. Если ваш офисный IP-адрес динамический, укажите подсеть, предоставляемую VPN-сервером WireGuard, который вы развернули на том же VPS, и получайте доступ к панели управления только через туннель:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authИспользование одного общего пароля basicauth становится небезопасным, как только пяти приложениям требуются собственные учетные записи. Тот же слот middleware принимает forwardauth, который передает решение об аутентификации Authentik, серверу для единого входа (SSO). В результате панель управления и все остальные маршруты оказываются защищены единой точкой входа, доступ к которой можно отозвать в одном месте.
Docker socket и права root
/var/run/docker.sock — это API, позволяющий создавать контейнеры, которые монтируют / с хоста. Доступ к нему равносилен получению прав root на машине, а Traefik использует его для чтения меток.
Используйте :ro при монтировании, но чётко понимайте, что это даёт: файл socket становится доступным только для чтения. Это не блокирует запросы POST к Docker API, передаваемые через него. Настоящая защита заключается в том, чтобы никогда не предоставлять Traefik прямой доступ к socket, а установить между ними фильтрующий прокси:
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxyУдалите том с socket из конфигурации Traefik и укажите провайдеру адрес прокси:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik сохраняет права на чтение данных о контейнерах и сетях, но теряет возможность создавать новые объекты.
Межсетевой экран, порты и правило, в котором все ошибаются
Открыты два порта, плюс SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableОпубликованные порты Docker обходят ufw. Docker добавляет собственные правила iptables, которые обрабатываются раньше цепочек ufw, поэтому контейнер, запущенный с ports: ["3000:3000"], доступен из интернета, даже если в ufw настроен запрет. Защита здесь должна быть структурной, а не на уровне конфигурации межсетевого экрана: публикуйте порты только через Traefik, а для всех остальных контейнеров используйте networks: [proxy] и ничего больше. Если какой-то сервис действительно должен быть доступен на хосте, привязывайте его к loopback, "127.0.0.1:3000:3000".
Устранение неполадок: ошибки, с которыми вы столкнетесь
404 page not found, отдается через Traefik. Маршрут не найден. Наиболее вероятные причины в порядке убывания: у контейнера отсутствует traefik.enable=true (при установленном exposedByDefault=false); правило Host() не соответствует введенному вами имени; имя маршрутизатора в одной метке отличается от имени в другой (routers.gitea.rule и routers.gitea.entrypoints должны быть одним и тем же словом); или вы заключили имя хоста в кавычки вместо обратных апострофов. Traefik v3 требует использования обратных апострофов внутри матчеров.
502 Bad Gateway. Маршрут найден, но backend недоступен. Почти всегда это означает, что контейнер не подключен к сети proxy, проверьте docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. Другая причина — неверный loadbalancer.server.port: вы указали опубликованный порт, либо приложение слушает другой порт. В логах указана попытка соединения: dial tcp 172.18.0.5:8080: connect: connection refused.
Браузер выдает предупреждение, сертификат выдан на TRAEFIK DEFAULT CERT. Для этого имени хоста не существует сертификата, и Traefik отдал свой самоподписанный сертификат-заглушку. Изучите строки ACME:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comDNS еще не указывает на ваш сервер. Исправьте запись, дождитесь истечения TTL и перезапустите Traefik.
Invalid response from http://git.example.com/.well-known/acme-challenge/... при прохождении HTTP-челленджа: порт 80 недоступен для Traefik извне. Обычно это сетевой экран на стороне провайдера перед VPS, а не ufw.
Сертификаты не выпускаются, а ваш DNS находится на Cloudflare с включенным «оранжевым облаком». Cloudflare терминирует TLS на своем уровне, и TLS-ALPN-01 не может завершиться через него. Переключите запись в режим «только DNS» на время выпуска или перейдите на использование DNS-01 челленджа с API-токеном. DNS-01 — это также единственный способ выпуска wildcard-сертификатов.
Циклическая переадресация (Redirect loop). Что-то перед Traefik уже терминирует TLS и пересылает открытый текст на :80; редирект в entrypoint отправляет запрос обратно на HTTPS. Удалите один из двух редиректов.
Поддержание работы
Юнит Docker должен быть разрешён для автозапуска (systemctl is-enabled docker), а restart: unless-stopped восстанавливает стек после перезагрузки. Для явного управления небольшой юнит systemd, запускающий docker compose -f /srv/edge/compose.yml up -d с параметром RemainAfterExit=yes, предоставляет systemctl status edge и контроль порядка выполнения.
Закрепите версию Traefik с помощью тега (traefik:v3.5, никогда не используйте latest). Обновление с v2 до v3 изменило синтаксис правил и имена провайдеров, поэтому автоматическое выполнение latest приведёт к загрузке конфигурации, которую сервис больше не понимает. Обновляйте версию осознанно: изучите примечания к миграции, измените тег, выполните docker compose up -d traefik и следите за логами. Если вы всё ещё используете тег v2, руководство по миграции Traefik с v2 на v3 подробно описывает все переименования, режим совместимости и процедуру отката, которая сохранит ваши сертификаты.
Создавайте резервные копии ./letsencrypt и томов с данными каждого приложения. Traefik не хранит других состояний, которые нельзя было бы восстановить из файла compose.
Что ломается при масштабировании
Первый предел — это не пропускная способность, а сам сервер: один экземпляр Traefik на одном VPS становится единой точкой отказа для пяти приложений, а acme.json использует файловое хранилище, поэтому попытка записи из двух экземпляров Traefik приведет к повреждению данных. Масштабирование требует переноса хранилища сертификатов во внешнее хранилище или завершения TLS на другом уровне.
Второй предел — это долгоживущие соединения. Server-sent events, передача больших файлов и медленные клиенты могут превысить тайм-ауты ответа точки входа; --entryPoints.websecure.transport.respondingTimeouts.readTimeout и связанные с ним параметры writeTimeout и idleTimeout позволяют настроить эти значения. WebSockets проходят через прокси без дополнительной настройки.
Третий предел — это диск. --accesslog=true записывает данные в stdout, а драйвер Docker json-file сохраняет их бесконечно, если не установлены ограничения. Настройте logging.options.max-size для сервиса Traefik или записывайте access log в файл с последующей ротацией.
Для всего этого не требуется оркестратор. Нужен лишь сервер под вашим управлением с реальным IP-адресом и открытыми портами 80 и 443; один небольшой VPS — это весь список зависимостей.
FAQ
Нужно ли мне по-прежнему использовать certbot, если я использую Traefik?
Нет. ACME-резолвер в Traefik запрашивает и обновляет сертификаты для каждого имени хоста, которое он обслуживает, и сохраняет их в acme.json. Certbot остается подходящим инструментом, если TLS терминируется на стороне nginx или другого сервера; использование обоих инструментов для одних и тех же имен хостов лишь приведет к исчерпанию лимитов Let's Encrypt.
Почему мой контейнер возвращает 404 через Traefik?
Ошибка 404, возвращаемая Traefik, означает, что ни один роутер не подошел под запрос. Убедитесь, что контейнер содержит traefik.enable=true (обязательно, если задан exposedByDefault=false), что значение Host() совпадает с именем, которое вы ввели, и что имя роутера идентично во всех метках для этого приложения. В Traefik v3 в правилах сопоставления (matcher) также требуются обратные кавычки, а не обычные.
В чем разница между 404 и 502 в данном случае?
Ошибка 404 означает, что маршрутизация не была выполнена; 502 означает, что роутер сработал, но backend отклонил соединение. Типичные причины 502: контейнер не подключен к сети proxy или loadbalancer.server.port указывает на опубликованный порт, а не на тот, который приложение слушает внутри контейнера. В access log указан точный адрес, к которому пытался обратиться Traefik.
Достаточно ли монтировать Docker socket в режиме read-only?
Флаг :ro делает файл сокета доступным только для чтения, но не API за ним. Запросы POST по-прежнему проходят через него, а доступ к Docker API равносилен правам root на хосте. Более надежная конфигурация — использование контейнера docker-socket-proxy, показанного выше, который предоставляет Traefik только права на чтение информации о контейнерах и сетях, полностью блокируя запись.
Может ли Traefik выпустить wildcard-сертификат?
Только через DNS-01 challenge при наличии API-токена вашего DNS-провайдера. Методы TLS-ALPN-01 и HTTP-01 проверяют только одно имя хоста и не могут выпустить wildcard-сертификат. DNS-01 также является решением, если CDN, например Cloudflare, терминирует TLS перед вашим VPS, из-за чего два других метода проверки не могут завершиться.