Перейти к основному содержимому
  1. Posts/
  2. Traefik/

Установка Traefik в LXC-контейнер Proxmox как systemd-сервиса | Часть 2

··3438 слов·17 минут· loading · loading · ·
Stilicho2011
Автор
Stilicho2011
Пишу о homelab, self-hosting, автоматизации и open-source решениях
Оглавление
Traefik - This article is part of a series.
Part : This Article

Введение
#

В первой части мы подготовили LXC-контейнер, поставили Traefik как бинарник и запустили его в тестовом режиме - с открытым dashboard и DEBUG-логами, только чтобы убедиться, что всё в принципе стартует. Как вы наверно поняли, конкретно в моем случае, речь идет о переезде рабочего конфига, а не о создании с нуля, поэтому часть статьи написана с учетом данной специфики.

С тех пор проделана основная часть работы: финальный static config, TLS-сертификаты, ротация логов и полная раскладка dynamic config по файлам - включая перенос десятка сервисов, которые раньше маршрутизировались через Docker labels, а не через Traefik file provider.

Сам по себе переезд на новый хост закономерно потребовал построчно пройтись по всей конфигурации - и по сути превратился в полноценный аудит. В процессе всплыло немало интересного: и про сам Traefik, и про то, как были устроены отдельные сервисы вокруг него. Но давайте обо всем по порядку.


Получение API-токена Cloudflare
#

Прежде чем собирать финальный static config, нужен API-токен Cloudflare - он понадобится для DNS-01 challenge при выпуске сертификатов (в том числе wildcard, без необходимости открывать порт 80 наружу под HTTP-01).

Конечно не обязательно пользоваться Cloudflare, можно использовать любого днс-провайдера, с которым работает DNS Providers :: ACME client and library written in Go.

Переходим в профиль Cloudflare, в раздел создания токенов.

Переход в профайл
Создаем токен

Выбираем создание кастомного токена - не готовый шаблон, чтобы ограничить права минимально необходимым набором, а не выдавать токену больше, чем реально нужно.

Кастомный токен

Указываем права: Zone → DNS → Edit, и по возможности ограничиваем токен конкретной зоной (доменом), а не всеми зонами аккаунта сразу - так при компрометации токена ущерб ограничен одним доменом.

Настройки токена

Создаём токен.

Создание токена

Значение токена показывается только один раз - сохраните его сразу в менеджере паролей. Посмотреть его повторно через интерфейс Cloudflare не получится, только пересоздать заново.

Значения токена

Список уже созданных токенов доступен там же, на странице токенов.

Список токенов

Этот токен и есть значение CF_DNS_API_TOKEN, которое пойдёт в traefik.env - подробнее в разделе про TLS ниже.


Финальный static config
#

Отличия от тестового варианта из части 1:

global:                             # Глобальные настройки Traefik.
  checkNewVersion: true             # При запуске Traefik проверяет, вышла ли новая версия.
  sendAnonymousUsage: true          # Отправляет анонимную телеметрию разработчикам Traefik.
                                     # Никакие конфигурации или домены не передаются.
  # Можно отключить, если принципиально не используете телеметрию.
api:                                 # Настройки встроенного API и веб-интерфейса.
  dashboard: true                   # Включает Dashboard для просмотра роутеров,
                                     # сервисов, middleware и состояния Traefik.
  insecure: false                   # Не публиковать dashboard голым портом 8080 без аутентификации.
                                     # Доступ на http://IP-Traefik:8080/dashboard/ закрыт -
                                     # см. раздел про защиту dashboard через Basic Auth ниже.
  debug: true                       # Разрешает расширенную диагностическую информацию API.
                                     # Безопасно, если Dashboard защищён авторизацией
                                     # и не опубликован в Интернет без защиты.
  disableDashboardAd: true          # Убирает рекламный блок Traefik Labs из интерфейса dashboard.
entryPoints:                        # Точки входа - порты, на которых слушает Traefik.
  web:                              # HTTP (порт 80).
    address: ":80"                  # Прослушивать все интерфейсы на TCP-порту 80.
    forwardedHeaders:               # Настройки доверенных X-Forwarded-* заголовков.
      trustedIPs: &trustedIps       # YAML-якорь. Позже этот список будет повторно использован
                                     # для HTTPS без дублирования.
        # Cloudflare public IP list (HTTP)
        - 103.21.244.0/22           # Разрешить доверять X-Forwarded-* только запросам,
                                     # пришедшим с известных IP-диапазонов Cloudflare.
        - 103.22.200.0/22
        - 103.31.4.0/22
        - 104.16.0.0/13
        - 104.24.0.0/14
        - 108.162.192.0/18
        - 131.0.72.0/22
        - 141.101.64.0/18
        - 162.158.0.0/15
        - 172.64.0.0/13
        - 173.245.48.0/20
        - 188.114.96.0/20
        - 190.93.240.0/20
        - 197.234.240.0/22
        - 198.41.128.0/17
        - 2400:cb00::/32            # Далее - те же диапазоны Cloudflare, но для IPv6.
        - 2606:4700::/32
        - 2803:f800::/32
        - 2405:b500::/32
        - 2405:8100::/32
        - 2a06:98c0::/29
        - 2c0f:f248::/32
        # Внутренняя сеть - узел перед этим Traefik
        - 192.168.30.23/32          # Мой апстрим-узел (например, второй реверс-прокси
                                     # или балансировщик) перед этим экземпляром Traefik.
        - 127.0.0.1/32               # Локальные запросы с самого хоста.
    http:                           # Настройки HTTP для данного entryPoint.
      aliasHeadersStrategy: delete  # Удаляет заголовки-псевдонимы (например, X_Forwarded_For
                                     # с подчёркиванием вместо дефиса), которые backend может
                                     # спутать с каноническим заголовком. Опция появилась
                                     # в Traefik v3.7.12 (август 2026) - подробнее ниже.
      encodedCharacters:            # Какие закодированные символы разрешены в пути запроса.
        allowEncodedSlash: true     # Не декодировать "%2F" в "/".
                                     # Нужно некоторым приложениям и API.
        allowEncodedHash: true      # Не декодировать "%23" в "#".
                                     # Иногда требуется REST API и WebDAV.
        allowEncodedBackSlash: false      # "%5C" остаётся запрещён - обычно признак попытки
                                           # обхода фильтрации пути (path traversal).
        allowEncodedNullCharacter: false  # "%00" остаётся запрещён - классический null-byte
                                           # трюк для обхода проверок расширения файла.
        allowEncodedSemicolon: true       # Разрешить "%3B" - некоторым legacy-приложениям
                                           # нужен параметр вида ";jsessionid=...".
        allowEncodedPercent: true         # Разрешить закодированный сам символ "%".
        allowEncodedQuestionMark: true    # Разрешить "%3F" внутри пути, а не только как
                                           # начало query-строки.
      middlewares:                  # Middleware, применяемые ко всем HTTP-запросам.
        - crowdsec@file             # Глобальная защита от известных атакующих IP через
                                     # CrowdSec, применяется сразу ко всем сервисам.
        - rate-limit@file           # Ограничение количества запросов.
                                     # Защищает от простого флуда и перебора.
        #- warp-auto@file           # Интеграция с Cloudflare WARP - пока отключена,
                                     # плагин установлен, но не подключен на entrypoint.
      redirections:                 # Правила автоматических перенаправлений.
        entryPoint:                 # Перенаправление между entryPoint.
          to: websecure             # Все HTTP-запросы отправлять на HTTPS.
          scheme: https             # Использовать HTTPS.
  websecure:                        # HTTPS (порт 443).
    address: ":443"                 # Прослушивать TCP-порт 443.
    forwardedHeaders:
      trustedIPs: *trustedIps       # Использовать тот же список доверенных IP,
                                     # который объявлен выше.
    http:
      aliasHeadersStrategy: delete  # То же самое, что и на entrypoint web.
      encodedCharacters:            # Полностью повторяет настройки web -
        allowEncodedSlash: true     # правило одно: раз задано на одном entrypoint,
        allowEncodedHash: true      # нужно продублировать и на остальных, глобального
        allowEncodedBackSlash: false      # блока encodedCharacters на уровне static config
        allowEncodedNullCharacter: false  # не существует.
        allowEncodedSemicolon: true
        allowEncodedPercent: true
        allowEncodedQuestionMark: true
      middlewares:
        - crowdsec@file
        - rate-limit@file           # Ограничение запросов также работает и по HTTPS.
        #- warp-auto@file
    transport:                      # Таймауты обработки соединений.
      respondingTimeouts:
        readTimeout: 600s           # Максимальное время чтения запроса клиента.
                                     # Полезно для больших загрузок.
        writeTimeout: 600s          # Максимальное время отправки ответа.
        idleTimeout: 600s           # Через сколько закрывать неактивное соединение.
  metrics:                          # Отдельный entryPoint для Prometheus.
    address: ":8082"                # Метрики доступны только на порту 8082.
    http:
      aliasHeadersStrategy: keep    # Для служебных entrypoint (метрики, healthcheck)
                                     # заголовки-псевдонимы можно не трогать - здесь
                                     # нет внешнего непроверенного трафика.
  ping:                             # Отдельный entryPoint для healthcheck-запросов.
    address: ":8083"                # /ping отдаётся только на порту 8083, изолированно
                                     # от основного трафика на 80/443.
    http:
      aliasHeadersStrategy: keep
metrics:                            # Настройки экспорта метрик.
  prometheus:                       # Использовать формат Prometheus.
    entrypoint: metrics             # Отдавать метрики через entryPoint metrics.
    # Optional but recommended for deep visibility
    addEntryPointsLabels: true      # Добавлять метку entryPoint в метрики.
    addServicesLabels: true         # Добавлять имя backend-сервиса.
    addRoutersLabels: true          # Добавлять имя router.
    # Defines duration buckets for latency metrics
    buckets:                        # Границы гистограмм времени ответа.
      - 0.1                         # До 100 мс.
      - 0.3                         # До 300 мс.
      - 1.2                         # До 1.2 секунды.
      - 5.0                         # До 5 секунд.
ping:                                # Включает сам механизм healthcheck (/ping).
  entryPoint: ping                  # Отдавать его через entryPoint ping, объявленный выше,
                                     # а не через web/websecure вместе с обычным трафиком.
tracing:                             # Экспорт трейсов запросов (OpenTelemetry).
  serviceName: traefik              # Имя сервиса, под которым трейсы видны в системе трейсинга.
  # Trace every request while validating the setup.
  # You can lower this later.
  sampleRate: 0.10                  # Трейсить 10% запросов - достаточно, чтобы видеть картину,
                                     # но не заваливать хранилище трейсов на каждый чих.
  # Optional: include Traefik internal resources such as ping@internal.
  addInternals: false               # Не трейсить обращения к самому Traefik (dashboard, ping).
  resourceAttributes:                # Дополнительные метки, которые уедут вместе с трейсами.
    deployment.environment: homelab
    service.instance.id: container_name    # Подставьте реальное имя контейнера/хоста.
    host.name: your_machine_hostname       # Подставьте реальный hostname машины.
  otlp:                              # Экспорт по протоколу OTLP.
    grpc:
      endpoint: alloy:4317          # Адрес коллектора - в моём случае Grafana Alloy.
      insecure: true                # Без TLS до коллектора - тот в той же локальной сети.
serversTransport:                   # Настройки подключения Traefik к backend.
  insecureSkipVerify: false         # Проверять TLS-сертификаты backend.
                                     # Это правильное значение.
                                     # Для отдельных сервисов можно создать
                                     # собственный ServersTransport.
providers:                          # Источники конфигурации.
  file:                             # Использовать файловый provider.
    directory: /etc/traefik/dynamic # Каталог с динамической конфигурацией.
    watch: true                     # Автоматически применять изменения без перезапуска.
certificatesResolvers:              # Настройки получения сертификатов.
  cloudflare:                       # Имя resolver.
    acme:                           # Использовать ACME (Let's Encrypt).
      email: email@email.com        # Email владельца сертификатов.
      storage: /etc/traefik/acme.json            # Где хранить сертификаты и аккаунт ACME.
      dnsChallenge:                 # Подтверждение владения доменом через DNS.
        provider: cloudflare        # Использовать Cloudflare API.
        resolvers:                  # DNS-серверы, через которые проверяется TXT-запись.
          - "1.1.1.1:53"
          - "1.0.0.1:53"
log:                                 # Основной журнал Traefik.
  level: "INFO"                     # Уровень логирования.
                                     # DEBUG полезен только при поиске проблем.
  filePath: "/var/log/traefik/traefik.log"
                                     # Путь к файлу логов.
  format: "common"                  # Классический текстовый формат.
  maxSize: 100                      # Ротация после достижения 100 МБ.
  maxBackups: 0                     # Хранить все архивы.
  maxAge: 0                         # Не удалять старые архивы по возрасту.
  compress: true                    # Сжимать старые файлы логов.
accessLog:                          # Журнал всех HTTP-запросов.
  filePath: "/var/log/traefik/access.log"
  format: json                      # JSON удобно анализировать через Loki,
                                     # Elasticsearch и Grafana.
  addInternals: true                # Логировать внутренние сервисы Traefik
                                     # (Dashboard, Ping и т.п.).
  filters:                          # Фильтрация записей.
    statusCodes:
      - "204-299"                   # Логировать успешные ответы.
      - "400-599"                   # Логировать ошибки клиента и сервера.
  fields:
    headers:
      defaultMode: drop             # По умолчанию не сохранять HTTP-заголовки.
      names:
        User-Agent: keep            # Оставить только User-Agent.
experimental:                       # Экспериментальные возможности.
  plugins:                          # Подключение сторонних плагинов.
    bouncer:                        # CrowdSec Bouncer.
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
                                     # GitHub-модуль плагина.
      version: v1.7.1               # Фиксированная версия.
    traefikwarp:                    # Плагин интеграции с Cloudflare WARP.
      moduleName: github.com/l4rm4nd/traefik-warp
      version: v1.1.5
tls:                                 # Глобальные настройки TLS.
  options:
    default:                        # Настройки по умолчанию для всех HTTPS-соединений.
      sniStrict: true                # Запрещать TLS-соединение без корректного SNI.
                                     # Повышает безопасность.
      minVersion: VersionTLS12      # Минимально допустимая версия TLS.
                                     # TLS 1.0 и 1.1 полностью запрещаются.
      cipherSuites:                 # Разрешённые шифры (используются только TLS 1.2).
                                     # Для TLS 1.3 список игнорируется,
                                     # так как он фиксирован стандартом.
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305

Главное, что поменялось относительно тестового конфига из первой части:

  • providers.docker убран полностью - цель всего переезда как раз в отвязке от Docker и любого другого бекэнда в принципе;
  • api.insecure явно выставлен в false - dashboard защищён отдельным роутером с Basic Auth (см. раздел ниже), а не голым портом 8080;
  • log.level вернул на INFO - DEBUG был нужен только для первого теста;
  • добавлен отдельный entryPoint ping и включён сам механизм ping: - healthcheck теперь живёт на собственном порту 8083, отдельно от обычного трафика;
  • добавлен блок tracing с экспортом в Grafana Alloy по OTLP - до этого трейсинга не было вообще, только метрики и логи.
Note

Актуализация (август 2026). Часть новых полей в конфиге выше - не моя инициатива, а следствие реальных изменений в самом Traefik. В релизе v3.7.12 (26 августа 2026) появилась опция aliasHeadersStrategy - она защищает от подмены заголовков через альтернативные способы записи одного и того же имени (Go канонизирует имена заголовков только по дефисам, поэтому X-Auth-User, X_Auth_User и X.Auth.User для него - три разных заголовка, а backend, приводящий заголовки к переменным окружения по стандарту CGI/WSGI, увидит их как один и тот же X_AUTH_USER). Опция заменяет более узкую underscoreHeadersStrategy, добавленную чуть раньше, в v3.6.20, и обрабатывает уже не только подчёркивание, а весь набор символов, которые HTTP разрешает в имени заголовка, кроме букв, цифр и дефиса. Если у вас Traefik новее v3.7.12 - aliasHeadersStrategy: delete стоит держать явно прописанным, а не полагаться на дефолт, по той же причине, что и с encodedCharacters ниже.

Про api.debug: true
#

Оставил осознанно. Это включает pprof-эндпоинты рядом с dashboard - сам по себе некритично, если роутер к api@internal защищён аутентификацией (Basic Auth или Authentik), что у меня и сделано.

Про encodedCharacters
#

Расширил список явных флагов - помимо allowEncodedSlash/allowEncodedHash, которые были и раньше, добавил allowEncodedBackSlash, allowEncodedNullCharacter (оба явно false - это классические пути обхода фильтрации, разрешать их незачем), а также allowEncodedSemicolon/allowEncodedPercent/allowEncodedQuestionMark (нужны отдельным приложениям и API, которые гоняют что-то нетривиальное прямо в пути запроса).

Само наличие явного блока, а не дефолтных значений, оставляю осознанно, хотя формально часть флагов дублирует поведение Traefik “из коробки”. У этих настроек нестабильная история между релизами v3 - дефолты менялись туда-обратно между минорными версиями, менялись разработчиками молча, что приводило к неработоспособным приложениям, типа Matrix или Trilium. Явная фиксация в конфиге страхует от неожиданностей при обновлении бинарника - ровно то же самое соображение, что и с новым aliasHeadersStrategy из заметки выше.


TLS: один wildcard-сертификат вместо кучи отдельных
#

Вместо того чтобы прописывать certResolver в каждом сервисном роутере отдельно, вынес это в отдельный файл dynamic/tls.yaml:

nano /etc/traefik/dynamic/tls.yml
tls:
  stores:
    default:
      defaultGeneratedCert:
        resolver: cloudflare
        domain:
          main: domain.ru
          sans:
            - "*.domain.ru"

defaultGeneratedCert задаёт сертификат по умолчанию для всего Traefik. Любой роутер с tls: {} (без явных domains/certResolver) автоматически получает этот wildcard-сертификат по SNI - не нужно дублировать резолвер в каждом из полутора десятков сервисных файлов.

Токен Cloudflare, полученный в самом начале статьи, для DNS-01 challenge вынесен из static config в отдельный EnvironmentFile:

sudo touch /etc/traefik/traefik.env
sudo chmod 600 /etc/traefik/traefik.env
# /etc/traefik/traefik.env, права 600
CF_DNS_API_TOKEN=ваш_токен

и подключён в systemd-юните через EnvironmentFile= - подробнее в разделе про юнит ниже.


Dashboard: защита через Basic Auth
#

Раз api.insecure больше не используется, доступ к dashboard идёт через обычный роутер - точно так же, как к любому проксируемому сервису, только с обязательным middleware аутентификации на нём.

Хеш пароля генерируется через htpasswd (пакет apache2-utils, который ставили ещё в части 1):

htpasswd -nB admin

Флаг -n выводит результат сразу в консоль, не записывая в файл; -B - использовать bcrypt. Отдельно подчеркну: не используйте openssl passwd -1 - это устаревший MD5-based хеш, который слабее криптографически и не рекомендуется для новых конфигураций.

Middleware:

http:
  middlewares:
    auth:
      basicAuth:
        users:
          - "admin:$2y$05$..."   # хеш из htpasswd -nB
        realm: "Restricted"

Роутер для dashboard подключает этот middleware и указывает специальный зарезервированный сервис api@internal - под этим именем сам Traefik публикует собственный dashboard и API, отдельного блока services: для него заводить не нужно:

http:
  routers:
    dashboard:
      entryPoints:
        - websecure
      rule: "Host(`traefik.domain.ru`)"
      service: api@internal
      middlewares:
        - auth
      tls: {}

tls: {} без явного certResolver - раз уже настроен wildcard-сертификат через defaultGeneratedCert (см. раздел про TLS выше), поддомен dashboard попадает под него автоматически, отдельно прописывать резолвер не нужно.


Ротация логов
#

Отдельный и очень важный момент, связанный с логами, а вернее с их размером: параметры maxSize/maxBackups/maxAge/compress в static config относятся только к log: (сам traefik.log). Для accessLog: встроенной ротации не существует вообще - Traefik умеет только переоткрыть файл по сигналу USR1, ротацией должен заниматься внешний logrotate.

Поэтому, чтобы избежать разрастания access.log до сотен мегабайт, я сделал следующее:

nano /etc/logrotate.d/traefik-access
# /etc/logrotate.d/traefik-access
# Правила автоматической ротации access.log Traefik.
/var/log/traefik/access.log {
    daily                   # Проверять необходимость ротации ежедневно.
    size 20M                # Выполнить ротацию, если размер файла достиг 20 МБ.
                             # При использовании вместе с daily ротация произойдёт,
                             # когда выполнится хотя бы одно из условий
                             # (прошёл день или превышен размер).
    rotate 14               # Хранить 14 архивов логов, затем удалять самые старые.
    compress                # Архивировать старые логи с помощью gzip.
    delaycompress           # Самый свежий архив не сжимать до следующей ротации.
                             # Полезно, если приложение ещё некоторое время
                             # может держать файл открытым.
    missingok               # Не считать ошибкой отсутствие файла логов.
    notifempty               # Не выполнять ротацию, если файл пустой.
    postrotate               # Команды, выполняемые после завершения ротации.
        systemctl kill -s USR1 traefik.service
                             # Отправить Traefik сигнал USR1.
                             # Traefik закроет старый файл журнала и
                             # откроет новый без перезапуска сервиса.
    endscript                # Конец блока postrotate.
}

traefik.log при этом продолжает ротироваться встроенным механизмом из static config - два независимых механизма для двух разных файлов, каждый под свою задачу.


Структура dynamic config
#

Раскладка по каталогам:

/etc/traefik/dynamic/
├── middlewares/
│   ├── auth.yaml
│   ├── authentik-forwardauth.yaml
│   ├── default-headers.yaml
│   ├── https-redirect.yaml
│   ├── ipAllowList.yaml
│   ├── middlewares-buffering.yaml
│   ├── nextcloud-secure-headers.yaml
│   ├── onlyoffice-middleware.yaml
│   ├── rate-limit.yaml
│   ├── secure-headers.yaml
│   └── ... (библиотека остальных middleware Traefik про запас)
├── services/
│   ├── dashboard.yaml
│   ├── nextcloud.yaml
│   ├── onlyoffice.yaml
│   ├── authentik.yaml
│   ├── homepage.yaml
│   ├── plex.yaml
│   ├── jellyfin.yaml
│   └── ... (по одному файлу на сервис)
├── serverstransports.yaml
└── tls.yaml

Один файл - один сервис или один middleware. Наименование файла сразу говорит, что внутри - не нужно открывать, чтобы понять назначение. Ко всему прочему, если всё описывать в одном файле, получится огромный по объёму YAML, и воспринимать информацию будет очень сложно.

Соответственно для создания middleware для базовой аутентификации, указанной выше, создаем командой

nano /etc/traefik/dynamic/middlewares/auth.yaml

Динамическую конфигурацию для dashboard создаем командой

nano /etc/traefik/dynamic/services/dashboard.yaml

Библиотека middleware про запас
#

Помимо тех, что реально используются, завёл файлы для всех middleware из официального списка Traefik OSS, которых ещё не было в структуре: AddPrefix, Chain, CircuitBreaker, Compress, ContentType, DigestAuth, Errors, GrpcWeb, InFlightReq, PassTLSClientCert, ReplacePath(Regex), Retry, StripPrefix(Regex). Не все подключены к реальным роутерам - часть лежит как готовый шаблон на случай, если понадобится (например, InFlightReq пригодится для Jellyfin/Immich при транскодировании, IPAllowList - если когда-нибудь понадобится ограничить доступ к части сервисов по IP). Но у меня на сайте еще будет отдельная статья, посвященная механизму middlewares в Traefik.


Перенос сервисов с Docker labels на file provider
#

Больше десятка сервисов (OnlyOffice, Authentik, Audiobookshelf, Dozzle, Forgejo, Komodo, Gotify, Linkwarden, Mealie, Navidrome, весь *arr-стек, Portainer, Trilium, Uptime Kuma, TubeArchivist) до этого маршрутизировались напрямую через Docker labels на самих контейнерах. При переносе в file provider аудит каждого конфига по пути показал несколько вещей, которые стоит упомянуть отдельно - часть из них годами работала незаметно именно потому, что Docker резолвит container name как DNS сам.

Docker labels - не все они складываются, а перезаписываются
#

Самая неочевидная находка аудита: если один и тот же лейбл (например, traefik.http.routers.X.middlewares) объявлен на контейнере несколько раз с разными значениями - Traefik использует последнее объявление, предыдущие тихо отбрасываются. У одного из моих сервисов (а точнее у OnlyOffice) таких дублей было три подряд - реально применялось только последнее, а два предыдущих просто ни на что не влияли, никак не сигнализируя об этом, и я жил в неведении.

Docker DNS-имена не резолвятся вне Docker-сети
#

Очевидная на бумаге вещь, но про которую очень легко забыть: адреса вида http://authentik_server:9000 или crowdsec:8080 работают только внутри одной и той же Docker-сети. При переезде Traefik в LXC все такие адреса пришлось заменить на реальные IP Docker-хоста - включая переменные окружения самих приложений (например, TRUSTEDREVERSEPROXY/whitelist-параметры у Trilium и Navidrome, которые доверяют заголовкам от конкретной подсети).

OIDC-клиент vs forwardAuth - не путать
#

В стеке оказалось два разных паттерна аутентификации через Authentik, и важно не смешивать их:

  • forwardAuth middleware на Traefik - нужен приложениям, которые сами не умеют в OIDC (Dozzle, Navidrome, весь *arr-стек, Uptime Kuma). Traefik перехватывает запрос до приложения и проверяет сессию через Authentik.
  • Встроенный OIDC-клиент в самом приложении - Gotify, Mealie, Trilium, Vaultwarden, Komodo умеют говорить с Authentik напрямую как Identity Provider. Здесь authentik@file middleware на Traefik не нужен и был бы избыточен - приложение уже требует логин само.

Финальный systemd-юнит
#

В принципе начальная настройка обратного прокси завершена. Однако осталось сделать ещё одну вещь - превратить наш обратный прокси в полноценный сервис, чтобы можно было использовать обычные команды systemctl.

nano /etc/systemd/system/traefik.service
# /etc/systemd/system/traefik.service
# systemd unit для запуска Traefik как системного сервиса.
[Unit]
Description=Traefik Reverse Proxy
# Краткое описание сервиса, отображается в systemctl status.
Documentation=https://doc.traefik.io
# Ссылка на официальную документацию Traefik.
After=network-online.target
# Запускать Traefik только после того, как сеть будет полностью поднята.
# Это особенно важно при использовании DNS Challenge и удалённых backend.
Wants=network-online.target
# При запуске Traefik также попытаться активировать network-online.target.
# В отличие от Requires, не считается ошибкой, если цель не будет достигнута.
[Service]
Type=simple
# Traefik работает как обычный процесс на переднем плане.
# systemd считает сервис запущенным сразу после старта процесса.
EnvironmentFile=/etc/traefik/traefik.env
# Загружает переменные окружения из файла.
# Обычно здесь хранятся секреты, например токен Cloudflare,
# чтобы не размещать их в конфигурации Traefik.
ExecStart=/usr/local/bin/traefik --configfile=/etc/traefik/traefik.yaml
# Команда запуска Traefik.
# В качестве конфигурации используется файл traefik.yaml.
Restart=on-failure
# Автоматически перезапускать сервис,
# если он завершился с ошибкой.
RestartSec=5
# Перед повторным запуском подождать 5 секунд.
# Это предотвращает бесконечный цикл мгновенных перезапусков.
[Install]
WantedBy=multi-user.target
# Запускать сервис автоматически при загрузке системы
# после достижения стандартного многопользовательского режима.

Работает от root (осознанный выбор для этого homelab) - поэтому AmbientCapabilities=CAP_NET_BIND_SERVICE не нужна, но если решите перейти на непривилегированного пользователя, это первое, что нужно будет добавить вместе с User=/Group=.

systemctl daemon-reload
systemctl enable --now traefik.service

Теперь доступны обычные команды:

systemctl status traefik    # статус юнита
systemctl restart traefik   # перезапуск после правки static config
systemctl stop traefik      # остановка

Что осталось сделать в будущем.
#

Сознательно оставил на отдельный заход, не в рамках этой статьи:

  1. Установка и настройка CrowdSec рядом с Traefik. По плану - через apt, локальный LAPI на 127.0.0.1:8080, acquis.yaml читает access.log напрямую с диска, новый bouncer-ключ вместо старого (тот, что раньше лежал в открытом виде в Docker-конфиге, уже отозван). Пока не установлен - соответствующий middleware в entrypoints static config закомментирован.
  2. Проброс портов бэкендов на Docker-хосте - сейчас часть сервисов (OnlyOffice, Authentik и другие) недоступны из нового Traefik-сегмента, порты нужно опубликовать на конкретном IP хоста и разрешить точечно через firewall-правила между сегментами сети.
  3. Разрешение конфликта портов - Portainer и Authentik оба целятся на 9000 при проксировании, нужно развести.
  4. Переключение реальных доменов со старого Docker-based Traefik на новый - пакетами, с проверкой каждого сервиса, а не разом.

Как только это будет сделано и новый Traefik примет боевой трафик - старый Docker-контейнер можно будет выключить. Об этом, вероятно, будет отдельная третья часть.

Ссылки
#

Traefik - This article is part of a series.
Part : This Article

Related

Установка Traefik в LXC-контейнер Proxmox как systemd-сервиса | Часть 3: CrowdSec

··1382 слов·7 минут· loading · loading
Третья часть серии про Traefik в LXC - установка CrowdSec в тот же контейнер, что и сам Traefik. Коллекции сценариев, профиль реагирования с уведомлениями в Gotify, источники логов (Traefik, syslog, AppSec), и подключение bouncer-плагина обратно к Traefik.

Установка Traefik в LXC-контейнер Proxmox как systemd-сервиса | Часть 1

··1441 слово·7 минут· loading · loading
Подробная инструкция по установке и настройке Traefik в LXC-контейнере для организации обратного прокси. Рассмотрены настройка Docker, конфигурация маршрутизации, интеграция с SSL и рекомендации по управлению веб-трафиком.

Middlewares в Traefik: что это, зачем нужны и полный список для homelab

·1778 слов·9 минут· loading · loading
Middleware - второй по значимости строительный блок Traefik после роутеров. В этой статье разбираю, что это такое, зачем нужно, как применяется, что такое chain, и прохожусь по всем middleware из открытой версии Traefik с примерами конфигурации, которые использую у себя.