Стыдно признаться, я еще практикую Swarm в 2025 году. Его легко объяснять, быстро готовить и просто использовать. Это инструмент старта в мир контейнеров, когда большой Kubernetes избыточен. Но путь развития Docker оказался не самым прямым. Со временем стало не хватать минимального динамического прокси для Swarm. Такого, который можно запустить один раз, чтобы он сам настраивал маршрутизацию трафика к микросервисам — и забыть. Так Millau — ingress-прокси и балансировщик на лейблах. Сейчас он обслуживает собственный сайт и ещё несколько проектов.

Зачем вообще нужен ещё один прокси?

Если вы разворачивали сервисы в Docker Swarm, то, сталкивались с вопросом маршрутизации внешнего трафика: как организовать доступ извне к нужным сервисам внутри стека. Можно, конечно, использовать Nginx, HAProxy или Traefik, и многие это делают. Но у этих решений есть свои минусы. Например, Nginx и HAProxy - классика, но требуют постоянной пересборки и деплоя файла конфига. Traefik - отличный универсальный инструмент, но его гибкость и архитектура middleware может приводить к чрезмерной усложнённости конфигурации.

На мой взгляд, задача проксирования должна быть максимально проста и свободна от всего, что хоть как-то напоминает бизнес-логику. Если вы дебажили переписывание заголовков или подмену частей URL сервером после очередного развертывания, то понимаете о чем я. Метапрограммирование в прокси еще читабельно в виде файла, и синтаксис может быть проверен линтером. Но в labels этот же конфиг превращается в неподдерживаемую «лапшу» параметров с разделителями. Мне же хотелось получить простое и декларативное решение, с самонастройкой, которое не требует редеплоя и будет полностью прозрачно для клиентов перед, и микросервисов за.

Как это работает

С точки зрения Docker, Millau это контейнер, который имеет readonly доступ к сокету Docker Engine для прослушивания событий. Создание, обновление, удаление сервисов (режим Swarm) и контейнеров (режим Compose) вызывает обновления правил проксирования. Сервис сообщает Millau о себе через метаинформации в labels. Например, чтобы добавить новый сервис с портом 9000 в Swarm стек, достаточно указать две метки в его манифесте:

deploy:
  labels:
    - "millau.enabled=true"
    - "millau.port=9000"

После развёртывания сервиса Millau обнаружит метки и добавит новый маршрут до него. По-умолчанию, проксироваться будет любой HTTP хост и любой HTTP путь. Всё. Это типичный кейс развёртывания SPA на один домен.

Если доменов больше, нужно сообщить Millau какие именно хосты этот сервис обслуживает. Добавляется третья метка в манифест сервиса:

deploy:
  labels:
    - "millau.enabled=true"
    - "millau.port=9000"
    - "millau.hosts=example.com"

Но что если сервисов больше? Например, другой типичный кейс развёртывания - микросервисы React frontend и API backend. Оба находятся на том же домене, и единственная разница - это префикс /api/ в URL backend. Добавляется четвертая метка в манифест:

# backend
deploy:
  labels:
    - "millau.enabled=true"
    - "millau.port=9000"
    - "millau.hosts=example.com"
    - "millau.path=/api/"

Метки для frontend можно оставить как есть. Все запросы с префиксом /api/ будут маршрутизированы в backend, а все остальные будут отданы frontend.
Детальное описание механизма сопоставления Host и Path Matching с примерами есть в документации на сайте.

Гарантированная доставка

Ключевая особенность - балансировка между несколькими сервисами. Millau умеет распределять трафик по ним и автоматически исключает те, которые ведут себя нестабильно. При отказе или замедлении (по-умолчанию 30 секунд) одного из сервисов запрос переключается на следующий активный. Через заданное время (по-умолчанию 60 секунд) прокси повторяет попытку и, если всё в порядке, возвращает сервис в работу. Если у сервиса несколько реплик, то перед тем как пометить его неактивным, прокси пробует сначала реплики.

Так можно развертывать разные версии (образы Docker) разного качества одного и того же микросервиса. Например, если blue, green и red обслуживают example.com, прокси отправляет трафик на них все по Round-Robin алгоритму. Если red падает, прокси помечает его как неактивный и переводит запрос на следующий - blue или green. Если время ответа blue вдруг станет превышать заданный для него таймаут, то прокси переводит запрос на green.

TLS и mTLS

Millau умеет терминировать HTTPS-трафик. Для этого достаточно передать публичный и приватный ключи TLS-сертификата через манифест. Ключи можно передавать напрямую или использовать переменные окружения - Docker подставит их значения автоматически во время развёртывания. KEY и CERT ниже должны быть закодированы в base64. Пример настройки:

deploy:
  labels:
    - "millau.enabled=true"
    - "millau.port=9000"
    - "millau.key=${KEY}"
    - "millau.cert=${CERT}"

Следующий типичный кейс - защита трафика между Cloudflare и кластером Docker Swarm. Для этого Cloudflare выпускает долгоживущий wildcard TLS-сертификат, который используется для шифрования соединения до самого микросервиса. После получения сертификата, его ключи экспортируются в переменные окружения выше. Стоит отдать им должное, услуга mTLS доступна на бесплатном тарифе.

Мониторинг

Millau предоставляет два типа метрик: для Docker healthcheck (код 200 для healthy, 503 для unhealthy статусов) и для интеграции с Prometheus.

Метрики Prometheus:

  • количество открытых подключений

  • количество полученных запросов

  • общий объём входящих и исходящих данных

  • количество успешно и неуспешно обработанных запросов

  • количество повторных попыток обработки запроса

  • текущий статус сервиса: 0 - недоступен, 1 - работает

  • гистограмма времени обработки запросов по микросервисам.

Для визуализации метрик собрал Grafana Dashboard.

Что внутри

Millau написан на Golang. Наружу открыты порты приема HTTP и HTTPS трафика. Дополнительно есть третий порт для телеметрии, но он по-умолчанию не доступен извне.

Для диагностики и отладки предусмотрена система логирования с пятью уровнями:

  • FATAL: критическая ошибка, после которой процесс Millau завершается;

  • ERROR: сбой в работе прокси, процесс продолжает работу;

  • WARN: некорректное поведение на стороне клиента или микросервисов, процесс продолжает работу;

  • INFO: штатный вывод, значение по умолчанию;

  • DEBUG: пошаговый вывод для разработки и анализа.

Чего нет

Millau сосредоточен на узкой нише Docker экосистемы: Swarm, Compose и Testcontainers. Kubernetes не поддерживается и в планах не значится.

Пока не реализована поддержка ACME протокола, поэтому автоматическое получение TLS-сертификатов через Let's Encrypt недоступно. Сертификаты необходимо копировать в манифест, либо передавать через переменные как на примере выше.

Постскриптум

Millau не коммерческий продукт. Прокси бесплатен для любого использования, но код пока закрыт. Я уважаю open source и многому научился у сообщества, и как только появляется возможность, возвращаю респект. Некоторыми моими опенсорсными проектами даже пользуются. Однако они редко выходят за пределы узкой ниши. Моя цель сделать не просто полезный код, а продукт с долгим жизненным циклом пока смерть Swarm не разлучит нас - выпустить Millau под открытой лицензией и сделать его стандартом для Docker Swarm. Получить лицензионный ключ можно за минуту на сайте. Каждый ключ, звезда или отзыв помогают привлечь внимание инвесторов и приближают к цели.

Если вы, как и я практикуете Swarm в 2025 и ищете простой ingress-прокси с самонастройкой, возможно, Millau вам подойдёт. Если вы используете Docker Swarm и сделали ingress иначе - будет интересно узнать, как. GitHub открыт для предложений, обсуждений, pull request'ов с тестами - их никогда не бывает много.

Update. Сайт проекта millau.net находится за Cloudflare. Хабравчане заметили, что не у всех открывается.

Комментарии (2)


  1. grigoryvp
    25.05.2025 19:54

    Это не то же самое, что делает Caddy-Docker-Proxy?


    1. olku Автор
      25.05.2025 19:54

      И да, и нет. Тоже читает лейблы, но создаёт по ним файл для Caddy, затем кладет файл в примонтированную папку, и перегружает Caddy. Я им пользовался до Millau. Парсер иногда ломает весь прокси, у автора висят issue которые нельзя решить из за ограничения такого подхода.