Когда проект перерастает один сервер, первым порогом становится надёжность: машина уходит в перезагрузку, и сайт молчит, пока она не вернётся. Kubernetes на этой дистанции стоит недели изучения и целый зоопарк компонентов. Docker Swarm приходит из коробки: он уже внутри движка, собирается тремя командами и решает главный вопрос - распределить сервисы по нескольким машинам с самовосстановлением. Ниже сборка кластера из трёх VPS: инициализация, порты, overlay-сети, сервисы, стеки и честные границы, за которыми всё же нужен Kubernetes.

Что даёт Swarm и почему кластер из трёх узлов минимум

Swarm - встроенный кластерный режим Docker: одна и та же служба движка превращает группу машин в единое целое. Менеджеры хранят состояние кластера по протоколу raft и решают, где запускать задачи, воркеры исполняют и отчитываются. Отдельного софта ставить не нужно: команда swarm init - и обычный Docker становится кластером.

Число три здесь не традиция, а арифметика. Состояние кластера живёт на менеджерах, и решения принимаются большинством - кворумом. Два менеджера не терпят ни одного сбоя: один упал, кворума нет, кластер замер в режиме только чтение. Три менеджера переживают один сбой и продолжают работать. Поэтому минимальный взрослый кластер - три машины, и на трёх VPS разумно сделать менеджерами все три.

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

Сравнение с одиночным сервером честнее всего показывает разницу. Compose на одной машине перезапускает упавшие контейнеры, но перезагрузка или потеря хоста останавливает всё сразу. Swarm держит реплики на разных машинах: узел пропал - задачи переселяются на соседей за секунды, публикация портов продолжается через оставшиеся. Для нагрузки из сотен пользователей разница между простоем в секунды и простоем в часы измеряется нервами и репутацией.

Инициализация кластера и подключение менеджеров и воркеров

На первом VPS кластер рождается одной командой с явным адресом:

docker swarm init --advertise-addr 203.0.113.10

В ответ машина печатает команду присоединения с токеном. Токен стоит сразу перевыпустить и держать в секрете: docker swarm join-token manager печатает свежую команду для менеджеров, join-token worker - для воркеров. Утечка токена означает приглашение в кластер кому попало, а перевыпуск занимает десять секунд.

На второй и третьей машине выполняется напечатанная команда вида:

docker swarm join --token SWMTKN-1-пример-токена 203.0.113.10:2377

Если узел присоединился как воркер, а роль должна быть менеджерской, она меняется на месте командой docker node promote с именем узла. Список узлов показывает docker node ls: имена, роли, статус и доступность. Лидер raft выбирается сам и помечается звёздочкой, вручную ничего настраивать не нужно.

Ещё одна тихая деталь - время. Протокол raft чувствителен к расхождению часов, поэтому на всех узлах включается синхронизация времени: systemd-timesyncd или chrony из пакета дистрибутива. Расхождение в минуты объясняет необъяснимые отказы кворума, и предупреждение об этом экономит часы отладки тому, кто собирает кластер в спешке.

Управлять кластером можно с рабочей машины, не заходя по ssh: docker context хранит подключение к удалённому движку.

docker context create swarm --docker "host=ssh://Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript."
docker context use swarm
docker node ls

После этого локальные команды работают с кластером напрямую, а docker context use default возвращает всё на место. Мелочь, а ощущение от эксплуатации совсем другое.

Проверка собранного кластера выглядит так:

  1. docker node ls перечисляет три узла со статусом Ready;
  2. docker info в секции Swarm отвечает словом active и числом узлов;
  3. тестовый сервис из трёх реплик распределяется по всем машинам;
  4. docker service ps показывает задачи с адресами узлов.

Порты фаервола, без которых узлы не найдут друг друга

Три порта объединяют кластер. Порт 2377 по tcp обслуживает управление: присоединение узлов и команды менеджерам. Порт 7946 в вариантах tcp и udp гоняет служебные сообщения между узлами. Порт 4789 по udp нужен сетям overlay - через него пакеты контейнеров летают между машинами.

sudo ufw allow from 203.0.113.11 to any port 2377 proto tcp
sudo ufw allow from 203.0.113.12 to any port 2377 proto tcp
sudo ufw allow 7946/tcp
sudo ufw allow 7946/udp
sudo ufw allow 4789/udp

Правила для 2377 стоит ограничить адресами соседних узлов: порт управления не обязан быть открыт миру. Порты 7946 и 4789 открываются шире, но и они - внутренняя механика кластера, и в идеальном мире узлы общаются через закрытую сеть хостера, где внешний фаервол вообще не нужен.

Диагностика соединений - самое частое место ошибок сборки: узел не присоединяется, время вышло. Первым делом проверяется связность по этим портам с обеих сторон, и только потом подозревают токен.

Если узлы стоят у одного хостера, первым кандидатом в идеальные сети становится их закрытая внутренняя сеть: трафик между машинами не покидает инфраструктуру, и правила фаервола сводятся к минимуму. Публичные адреса остаются только для входящих запросов к сайтам, а служебные порты живут в закрытом контуре, куда снаружи не достучаться.

Overlay сети и маршрутизация входящих запросов

Обычные сети Docker живут внутри одной машины, overlay-сеть связывает контейнеры на разных узлах. Одна команда:

docker network create -d overlay web

и сервисы, подключённые к сети web, видят друг друга по именам с любых машин кластера. Шифрование overlay-трафика включается флагом --opt encrypted, и для трафика между дата-центрами это разумная предосторожность с небольшим штрафом к скорости.

Входящие запросы обслуживает встроенная сеть ingress с так называемой routing mesh: порт, опубликованный сервисом, слушается на каждом узле кластера. Запрос может прилететь на любую из трёх машин, mesh доставит его живой реплике, где бы она ни находилась. Для маленького кластера это подарок: достаточно одного адреса в DNS, балансировка прилагается без настройки.

Тонкость, которая экономит нервы: имена сервисов резолвятся внутри overlay-сети, и виртуальный адрес балансирует запросы между репликами автоматически. Приложению не нужно знать топологию кластера - только имена соседей по сети.

Проверить связность изнутри удобно одноразовым контейнером в attachable-сети: имя сервиса отвечает, задержка измеряется прямо оттуда. Отдельное свойство mesh в том, что опубликованный порт отвечает даже на узле, где реплик нет: запрос просто уезжает туда, где живёт задача. Это же свойство позволяет ставить внешний балансировщик, который видит все три адреса и не заботится, где именно живут реплики.

Сервисы с репликами и обновление без остановки

Единицей работы в Swarm служит сервис с заявленным числом реплик:

docker service create \
  --name web \
  --replicas 3 \
  --network web \
  -p 80:80 \
  --update-parallelism 1 \
  --update-delay 30s \
  --update-failure-action rollback \
  nginx:1.28-alpine

Разбор заслуживает каждая строка. Реплики распределяются по узлам, опубликованный порт слушает mesh на всех машинах. Параметры обновления - сердце безостановочности: parallelism 1 заменяет реплики по одной, delay 30s выдерживает паузу между шагами, failure-action rollback откатывает неудачное обновление без участия человека.

Обновление сводится к смене образа:

docker service update --image nginx:1.29-alpine web

Масштабирование делается командой docker service scale web=6, состояние - через docker service ps web. Если у образа есть проверка здоровья, Swarm ждёт готовности новой реплики, прежде чем снять старую, и это лучший способ обновлять живой сервис незаметно для посетителей.

Проверка здоровья задаётся прямо в сервисе и переиспользуется при обновлениях:

--health-cmd "curl -f http://localhost/healthz || exit 1" \
  --health-interval 15s \
  --health-retries 3

С такой настройкой Swarm считает реплику готовой только после успешного ответа, а зависший при старте контейнер не получает трафика. Обновление из примера выше превращается в конвейер: новая реплика проверена, старая снята, пауза, следующая.

Развертывание стека из compose файла командой docker stack deploy

Для целых приложений у Swarm есть стеки: compose-файл с секцией deploy разворачивается одной командой docker stack deploy -c stack.yml mysite. В секции описываются реплики, ограничения размещения и параметры обновления:

services:
  web:
    image: mysite/web:2.4
    networks:
      - web
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 30s
        failure_action: rollback
      placement:
        constraints:
          - node.role == manager
  redis:
    image: redis:8-alpine
    networks:
      - web
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == vps-2
networks:
  web:
    driver: overlay
    attachable: true

Ограничения размещения - простой способ закрывать вопросы вида положи базу на конкретную машину. Флаг attachable разрешает обычным контейнерам подключаться к сети стека, что удобно для отладки без переписывания файлов.

Жизненный цикл стека короткий и предсказуемый: docker stack ls перечисляет стеки кластера, docker stack ps mysite показывает задачи со статусами, docker stack rm mysite снимает всё разом. Повторный deploy того же файла приводит стек к состоянию из файла, ровно как compose, только на весь кластер.

Ограничения стеков стоит знать заранее: секция build не работает, образы собираются заранее и живут в реестре, а именованные тома по умолчанию локальны на каждом узле. Для stateless-сервисов это не мешает вовсе, для баз - приглашение подумать о внешнем хранилище или закреплении за конкретным узлом.

Секреты в Swarm решают вопрос паролей без файлов .env. Значение хранится в зашифрованном журнале raft и выдаётся контейнерам в каталог /run/secrets:

docker secret create db_password ./db_password.txt
docker secret ls

В сервисе секрет подключается флагом --secret, а приложение читает его как обычный файл. Секреты обновляются командой docker service update с заменой, и пароли перестают путешествовать по серверам в открытом виде.

Эксплуатация кластера, drain и сценарии, где Swarm проигрывает

Обслуживание узла начинается командой drain:

docker node drain vps-2

После неё Swarm сводит задачи с узла и переселяет их на соседей, а узел помечается недоступным для новых задач. Обновили ядро, вернули машину - docker node activate возвращает её в работу, и балансировка подхватывает узел обратно.

Вывод из кластера симметричен: docker swarm leave на самом узле и docker node rm на менеджере очищают списки. Замена машины сводится к выводу старого узла и присоединению нового тем же токеном: состояние кластера хранится на менеджерах, переносить нечего.

Здоровье кластера мониторится штатными средствами: docker node ls показывает статусы, а слабое место маленьких кластеров одно - потеря кворума, после которой менеджеры переходят в режим только чтение. Тревога на изменение числа узлов или на недоступность любого из них ставится внешним мониторингом за минуты и спасает от тихой деградации.

Границы честности стоит проговорить. Swarm не умеет автомасштабирование по нагрузке, его экосистема инструментов беднее Kubernetes, а постоянные хранилища потребуют отдельных решений. Для трёх-пяти машин со stateless-сервисами, парой баз на закреплённых узлах и желанием спать спокойно этого более чем достаточно. Kubernetes начинает окупаться, когда узлов заметно больше, а команда готова содержать отдельную дисциплину.

Swarm - самый короткий путь от одного сервера к кластеру: три команды, три VPS, встроенный балансировщик и обновления без остановки. Он не заменит Kubernetes в больших садах, но на небольшой плантации делает ровно ту работу, за которую большой сосед просит неделю настройки и отдельного инженера. Три дешёвые машины с Swarm внутри закрывают тот же вопрос за вечер, включая фаервол, первый стек и проверку обновлений.