SSD Nodes Learn
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-07-24

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-auth

Docker 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:2375

Traefik сохранит право на чтение данных о контейнерах и сетях, но потеряет возможность создавать новые объекты.

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.com

DNS еще не указывает на этот сервер. Исправьте запись, дождитесь истечения 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, и другие два метода проверки не срабатывают.