Traefik v3 и Docker Compose: 5 приложений
Настройте маршрутизацию по Host rule и Let's Encrypt в Traefik v3. Узнайте, как избежать ошибки acme.json, мешающей запуску контейнеров в Docker Compose.
Один IP, пять приложений, один порт 443
Ваш VPS имеет один публичный IPv4-адрес и один TCP-порт 443. Вам необходимо разместить на нем Gitea, staging-копию вашего приложения, внутреннюю панель управления, страницу статуса и обработчик webhook — пять хостнеймов на одном сервере. Reverse proxy — это процесс, который занимает порты :80 и :443, считывает заголовок Host в каждом запросе и передает его в нужный контейнер. Traefik выполняет эту функцию и получает, а также обновляет сертификаты для каждого хостнейма без необходимости ручного запуска certbot.
Отличие 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 — это бэкенд: контейнер и порт, который он использует внутри сети Docker.
- Middlewares работают между роутером и сервисом: базовая аутентификация, списки разрешенных IP-адресов, изменение заголовков, перенаправления.
Статическая конфигурация (entrypoints, providers, ACME) передается через командную строку Traefik или в traefik.yml; ее изменение требует перезапуска Traefik. Динамическая конфигурация (routers, services, middlewares) поступает из labels контейнеров и перезагружается «на лету». Смешивание этих двух типов конфигурации — основная причина ошибки «флаг не работает».
Файл 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=3000Сначала docker compose up -d, затем docker compose logs -f traefik. Каждое дополнительное приложение является копией блока gitea со своим именем router, своим Host() и своим внутренним портом. Установка Nextcloud в Docker с TLS и резервным копированием настраивается аналогичным образом: уберите опубликованные порты, подключите приложение к proxy и используйте метки router для настройки hostname и сертификата.
Важны пять деталей.
exposedByDefault=false делает контейнер невидимым для Traefik, пока в нем не указан traefik.enable=true. Если не указать этот параметр, для каждого запущенного контейнера — включая временный postgres, использованный для проверки, — будет создан маршрут.
providers.docker.network=proxy указывает Traefik, какую сеть использовать, если контейнер подключен к нескольким сетям. Если этот параметр отсутствует, Traefik может выбрать неверный IP-адрес контейнера. Это приведет к ошибке 502, которую можно принять за сбой приложения.
loadbalancer.server.port=3000 — это порт внутри контейнера; в данном случае Gitea прослушивает порт 3000. Обратите внимание, что ни один контейнер приложения не публикует порты — это делает только Traefik.
Перенаправление на entrypoint web преобразует HTTP-запросы в 308 redirect на 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 для данного hostname уже указывал на ваш 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Верное решение — описанное выше: используйте bind-mount для директории, чтобы Traefik сам создал файл с нужным режимом доступа. Если вы создали acme.json с помощью touch, ваш umask установил права 644. Исправьте это на хосте:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikСоздавайте резервные копии этой директории вместе с томами вашего приложения. Потеря файла не критична — сертификаты можно перевыпустить, — но одновременный перевыпуск пяти hostname приведет к превышению лимитов (rate limits).
Используйте staging CA во время отладки. Раскомментируйте строку caserver, настройте все маршруты, затем закомментируйте её и удалите acme.json, чтобы запросить новые рабочие сертификаты. Production Let's Encrypt разрешает выпускать пять дублирующихся сертификатов в неделю для одного и того же набора hostname и ограничивает частоту повторных неудачных проверок для одного имени. Staging выпускает недоверенные сертификаты (браузер выдаст предупреждение, и это предупреждение подтвердит, что всё работает), при этом лимиты там гораздо мягче.
Панель управления — это инструмент администрирования, а не демо-версия
В большинстве руководств по быстрому старту настраивается --api.insecure=true, который запускает панель управления на порту 8080 без аутентификации. Это небезопасно для сервера с публичным IP, так как любой сканер сможет получить данные о топологии маршрутизации, именах хостов, названиях middleware и портах бэкенда.
Альтернативный вариант — использование параметров службы traefik, указанных выше. В этом случае панель управления работает как обычное приложение: на реальном хосте, через TLS и за прокси-сервером basicauth. Параметр service=api@internal связывает маршрутизатор со встроенным API Traefik. Для повышения безопасности можно добавить список разрешенных IP-адресов (IP allow-list), который применяется слева направо. Если у вашего офиса динамический IP, укажите в качестве диапазона подсеть, которую выдает WireGuard VPN, развернутый вами на том же VPS, и подключайтесь к панели управления только через этот туннель:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authDocker socket имеет права root
/var/run/docker.sock — это API, который позволяет создавать контейнеры с монтированием / из хост-системы. Доступ к этому API эквивалентен правам root на машине. Traefik необходим этот доступ для чтения labels.
Оставьте :ro в монтировании, но учитывайте последствия: это делает файл сокета доступным только для чтения. Это не предотвращает выполнение POST запросов к Docker API через этот сокет. Основной способ защиты — не передавать сокет напрямую в Traefik, а использовать между ними фильтрующий прокси:
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Удалите volume сокета из конфигурации Traefik и укажите провайдеру путь к прокси:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik сохранит право на чтение данных о контейнерах и сетях, но потеряет возможность создавать новые объекты.
Firewall, порты и правило, которое все понимают неправильно
Два открытых порта плюс 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 установлено правило deny. Проблема заключается в архитектуре, а не в конфигурации firewall: публикуйте порты только для Traefik, а для всех остальных контейнеров используйте networks: [proxy] и ничего больше. Если доступ к хосту действительно необходим, привязывайте его к loopback — "127.0.0.1:3000:3000".
Устранение неполадок: реальные ошибки
404 page not found, возвращаемая Traefik. Роутер не нашел совпадений. Возможные причины (от наиболее вероятной к наименее вероятной): в контейнере не установлен traefik.enable=true (с установленным exposedByDefault=false); правило Host() не соответствует введенному вами имени; имя роутера в одном label отличается от имени роутера в другом (routers.gitea.rule и routers.gitea.entrypoints должны быть идентичны); или вы заключили hostname в кавычки вместо обратных апострофов. Traefik v3 требует использования обратных апострофов внутри matchers.
502 Bad Gateway. Роутер нашел совпадение, но бэкенд недоступен. Почти всегда это происходит из-за того, что контейнер не находится в сети proxy — проверьте docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. Другая возможная причина — неверный loadbalancer.server.port: вы указали опубликованный порт, или приложение слушает другой порт. В логах указана попытка подключения: dial tcp 172.18.0.5:8080: connect: connection refused.
Браузер выдает предупреждение, а сертификат выдан на TRAEFIK DEFAULT CERT. Для данного hostname не существует сертификата, поэтому 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 challenge: порт 80 недоступен для Traefik извне — обычно это firewall на уровне провайдера перед VPS, а не ufw.
Сертификаты не выпускаются, а ваш DNS настроен на Cloudflare с включенным «оранжевым облаком». Cloudflare завершает TLS на своем узле, поэтому проверка TLS-ALPN-01 через него не может быть завершена. Установите режим DNS-only на время выпуска сертификата или переключитесь на DNS-01 challenge с использованием API token. DNS-01 — единственный метод проверки, позволяющий выпускать wildcard-сертификаты.
Redirect loop. Перед Traefik уже работает сервис, который завершает TLS и пересылает незашифрованный трафик на порт :80; перенаправление (redirect) на 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, загрузка больших файлов и медленные клиенты могут вызвать срабатывание таймаутов ответа на entrypoint. Настраивать их позволяют --entryPoints.websecure.transport.respondingTimeouts.readTimeout и его параметры writeTimeout и idleTimeout. WebSocket проходят без дополнительной настройки.
Третье ограничение — дисковое пространство. --accesslog=true записывает данные в stdout, а драйвер json-file в Docker хранит эти данные бесконечно, если не установить лимит. Установите logging.options.max-size для сервиса Traefik или записывайте access log в файл с последующей ротацией.
Для решения этих задач не требуется оркестратор. Необходим сервер под вашим контролем с публичным IP-адресом и открытыми портами 80 и 443 — это единственный обязательный компонент.
FAQ
Нужен ли мне Certbot, если я использую Traefik?
Нет. ACME-резольвер Traefik запрашивает и обновляет сертификат для каждого хоста, который он маршрутизирует, и сохраняет их в acme.json. Certbot остается подходящим инструментом, если TLS завершает сам nginx или другой сервер; одновременное использование обоих инструментов для одних и тех же хостов приведет к исчерпанию лимитов Let's Encrypt.
Почему мой контейнер возвращает 404 через Traefik?
Ошибка 404 от Traefik означает, что запрос не совпал ни с одним маршрутизатором. Убедитесь, что контейнеру присвоен traefik.enable=true (обязательно после установки exposedByDefault=false), что значение Host() совпадает с введенным вами именем, и что имя маршрутизатора идентично во всех label для этого приложения. В Traefik v3 внутри matcher должны использоваться обратные кавычки (backticks), а не обычные.
В чем разница между 404 и 502 в данном случае?
404 означает, что маршрутизация не была выполнена; 502 означает, что маршрутизатор нашел совпадение, но бэкенд отклонил соединение. Типичные причины 502: контейнер не подключен к сети proxy или loadbalancer.server.port указывает на опубликованный порт вместо порта, который приложение использует внутри контейнера. В access log указан точный адрес, к которому обращался Traefik.
Достаточно ли монтировать Docker socket в режиме read-only?
Флаг :ro делает файл сокета доступным только для чтения, но не сам API — POST запросы все равно проходят через него, а доступ к Docker API эквивалентен правам root на хосте. Более безопасным вариантом является контейнер docker-socket-proxy, показанный выше; он предоставляет Traefik только права на чтение данных контейнеров и сетей, полностью блокируя запись.
Может ли Traefik выпустить wildcard-сертификат?
Только через challenge DNS-01 с использованием API-токена вашего DNS-провайдера. Методы TLS-ALPN-01 и HTTP-01 проверяют только один хост и не могут создать wildcard-сертификат. DNS-01 также является единственным решением, если TLS завершается на CDN (например, Cloudflare) перед вашим VPS, и другие два метода проверки не срабатывают.