Новая страница
Docker Swarm с «edge-ingress» на Traefik, Cloudflare и CrowdSec: детальный разбор, грабли и устойчивая схема
Материал обезличен. Вместо реальных доменов используйте
YOU.DOMAIN. Вместо реальных IP — поясняющие подписи. Все команды — понятные, без автоподстановок через shell-переменные.
Эта статья — практический отчёт по реальной миграции «зоопарка» сервисов (SSO, прокси, CMS, внутренние приложения) на Docker Swarm с выделенным edge-узлом под ingress-контроллер (Traefik v3), фронтом через Cloudflare (orange cloud, без Tunnel), DDoS/банами от CrowdSec (nftables-bouncer) и без прямого доступа к docker.sock (через docker-socket-proxy).
Мы шаг за шагом разберём:
- почему 3 менеджера и отдельный edge-worker — хорошая базовая топология;
- как готовить сеть и секреты под ACME DNS-01 в Cloudflare;
- как правильно запускать Traefik в Swarm (host-порты только на edge, provider=swarm через socket-proxy, без монтирования
docker.sock); - диагностику каверзных проблем Swarm-DNS (когда сервисы «не видят» друг друга) — и как их чинить;
- нюанс с
port is missingв Traefik; - минимальную, но аккуратную операционку: firewall, sudo NOPASSWD, sanity-чеклисты.
1) Архитектура и обоснования
1.1 Менеджеры и edge-worker
-
3 менеджера в одном регионе/датацентре — золотой стандарт для Raft (нечётное число, кворум, отказоустойчивость).
-
Отдельный edge-узел = обычный worker с меткой, например
node.labels.edge=true. На нём живёт только ingress (Traefik) и, возможно, другие «краевые» вещи (но не храните там состояние и не делайте его manager). Это:- уменьшает поверхность атаки на Raft;
- даёт удобную точку фильтрации/логирования/проверок;
- позволяет жёстко контролировать публикации портов 80/443.
1.2 Traefik вместо NPM
Для Swarm это логично:
- встроенная интеграция с providers.swarm (сервисы находят друг друга по Overlay-DNS);
- полноценный ACME (в т.ч. DNS-01) и гибкая маршрутизация;
- безопасно, если не монтировать
docker.sock, а использовать docker-socket-proxy.
1.3 Cloudflare (orange cloud)
- DNS «за тучкой», без Cloudflare Tunnel;
- A/CNAME записи на edge-узел, включённое проксирование (оранжевая тучка);
- ACME DNS-01: Traefik расставляет TXT-записи через Cloudflare API token с минимальными правами.
1.4 CrowdSec + nftables и UFW
- В современном Ubuntu UFW часто работает поверх nftables. Два «дирижёра» одновременно — плохая идея.
- Решение: или чистый nftables (и bouncer), или UFW как фронтенд, но без конфликтов. Здесь выбран CrowdSec nftables-bouncer и UFW выключен.
2) Подготовка: пользователи и firewall
2.1 Sudo без пароля для админа
echo 'adminuser ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/90-adminuser
sudo chmod 440 /etc/sudoers.d/90-adminuser
Где adminuser — ваш пользователь.
2.2 Базовые порты Swarm межузлово (в обе стороны)
- 2377/TCP — управление Swarm (менеджер<->ноды)
- 7946/TCP+UDP — gossip (overlay-сеть)
- 4789/UDP — VXLAN (overlay-сеть)
В nftables добавляйте allow-правила до deny-цепочек (и не забывайте зеркалить на обеих сторонах). CrowdSec-списки не должны перекрывать эти порты.
3) Cloudflare: токен и DNS
3.1 Минимальные права токена
Создайте API Token уровня зоны:
- Zone → Zone → Read (чтение зоны)
- Zone → DNS → Edit (правка DNS — для TXT при DNS-01)
Ограничьте токен конкретной зоной (
YOU.DOMAIN).
3.2 Сохранение токена в секрет Swarm
printf '%s' 'PASTE_CF_DNS_API_TOKEN_HERE' | docker secret create cf_dns_api_token -
3.3 DNS-записи на edge
В Cloudflare:
Aзапись дляtraefik.YOU.DOMAIN→ публичный IP edge-узла, проксирование включено.- Аналогично для будущих сервисов (например,
whoami.YOU.DOMAIN), указывающих на тот же IP edge (Traefik разрулит внутри).
Примечание: держите список доверенных IP Cloudflare в Traefik в актуальном состоянии — они время от времени меняются. Мы покажем, куда вставлять.
4) Overlay-сети Swarm
Создайте две сети:
proxy— внешняя «маршрутизируемая» overlay-сеть для Traefik и бэкендов;dockerapi— internal overlay-сеть только для API (Traefik ↔ docker-socket-proxy).
docker network create -d overlay --attachable proxy
docker network create -d overlay --attachable --internal dockerapi
Проверка:
docker network ls
docker network inspect proxy
docker network inspect dockerapi
В dockerapi будет Internal: true — правильно.
5) Безопасный доступ к Docker API: docker-socket-proxy
Зачем: не монтируем docker.sock в Traefik, чтобы не превращать его в «root в коробке». Вместо этого запускаем прокси на менеджерах (где есть Swarm API), а Traefik ходит к нему по overlay-DNS.
5.1 Стек dockerapi-stack.yaml
version: "3.8"
services:
dockerproxy:
image: tecnativa/docker-socket-proxy:latest
# ЧИТАЕМ socket ТОЛЬКО в режиме ro
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# Подключаем К ТОЛЬКО внутренней сети dockerapi
networks:
dockerapi:
aliases:
- dockerproxy # имя, под которым Traefik будет стучаться
# Включаем только нужные эндпоинты Docker API
environment:
SERVICES: "1"
TASKS: "1"
NETWORKS: "1"
NODES: "1"
SWARM: "1"
INFO: "1"
LOG_LEVEL: "info"
deploy:
mode: global # на каждом менеджере
placement:
constraints:
- node.role == manager # только менеджеры
networks:
dockerapi:
external: true
Деплой и проверка:
docker stack deploy -c dockerapi-stack.yaml dockerapi
docker service ls
docker service ps dockerapi_dockerproxy
# Самотест из overlay-сети:
docker run --rm --network dockerapi curlimages/curl:8.8.0 -s http://dockerapi_dockerproxy:2375/_ping
# Должно вернуть: OK
Пояснение: VIP по умолчанию устраивает. Имя сервиса
dockerapi_dockerproxyдолжно резолвиться в overlay-DNS для любых контейнеров, подключённых к сетиdockerapi. Никаких «tasks.» и DNSRR не требуется, если всё в порядке с overlay.
6) Traefik v3 на edge-worker (ingress)
Только на edge-узле пробрасываем 80/443 в режиме host. Traefik подключён к двум сетям: proxy (наружу к бэкендам) и dockerapi (внутрь к API-прокси).
6.1 Секреты
-
Cloudflare API token (уже создан):
cf_dns_api_token -
BasicAuth для панели (рекомендуется через секрет):
printf '%s\n' 'admin:$apr1$PASTE_HTPASSWD_HASH' | docker secret create traefik_users -
Хеш можно сделать заранее утилитой
htpasswd. Пример ввода в секрет — уже готовый хеш.
6.2 Файловая структура
/app/traefik/
├── traefik-stack.yaml
└── dynamic/
└── (необязательно, если всё в labels)
6.3 Стек traefik-stack.yaml
version: "3.8"
services:
traefik:
image: traefik:v3.4
# Публикуем порты ТОЛЬКО на edge-узле (host-mode)
ports:
- target: 80
published: 80
protocol: tcp
mode: host
- target: 443
published: 443
protocol: tcp
mode: host
# НИКОГДА не монтируем docker.sock
volumes:
- acme:/letsencrypt # хранилище сертификатов
- /app/traefik/dynamic:/dynamic:ro # опционально: динамические правила
# Считываем Cloudflare токен из секрета
environment:
CF_DNS_API_TOKEN_FILE: /run/secrets/cf_dns_api_token
secrets:
- cf_dns_api_token
- traefik_users
command:
# === Entrypoints ===
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
- "--entrypoints.websecure.address=:443"
# Доверяем X-Forwarded-* ТОЛЬКО Cloudflare, чтобы видеть реальный клиентский IP
# Список сетей Cloudflare: актуализируйте по их документации
- "--entrypoints.web.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"
- "--entrypoints.websecure.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"
# === ACME (Let's Encrypt) DNS-01 через Cloudflare ===
- "--certificatesresolvers.le.acme.email=admin@YOU.DOMAIN"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.dnschallenge=true"
- "--certificatesresolvers.le.acme.dnschallenge.provider=cloudflare"
# === Провайдер Swarm (через docker-socket-proxy) ===
- "--providers.swarm=true"
- "--providers.swarm.endpoint=http://dockerapi_dockerproxy:2375"
- "--providers.swarm.watch=true"
- "--providers.swarm.exposedbydefault=false"
- "--providers.swarm.network=proxy" # сеть, где Traefik будет искать бэкенды
# === Дашборд и логи ===
- "--api.dashboard=true"
- "--api.insecure=false"
- "--accesslog=true"
- "--log.level=INFO"
networks:
- proxy
- dockerapi
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.labels.edge == true # привязываем к edge-узлу
labels:
# Включаем Traefik для самого Traefik (чтобы открыть дашборд)
- "traefik.enable=true"
# Роутер для панели
- "traefik.http.routers.traefik.rule=Host(`traefik.YOU.DOMAIN`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=le"
- "traefik.http.routers.traefik.service=api@internal"
# BasicAuth через секрет (файл с htpasswd-хешем)
- "traefik.http.middlewares.traefik-auth.basicauth.usersFile=/run/secrets/traefik_users"
- "traefik.http.routers.traefik.middlewares=traefik-auth@swarm"
# Критически важный нюанс!
# Когда вы "включили" Traefik как сервис (traefik.enable=true),
# Traefik ожидает "порт" у backend-сервиса. Даже если роутер указывает api@internal,
# объявите порт явно, иначе получите "port is missing".
- "traefik.http.services.traefik.loadbalancer.server.port=80"
networks:
proxy:
external: true
dockerapi:
external: true
volumes:
acme:
secrets:
cf_dns_api_token:
external: true
traefik_users:
external: true
Деплой и контроль:
docker stack deploy -c /app/traefik/traefik-stack.yaml traefik
docker service ps traefik_traefik
docker service logs -f traefik_traefik
Ожидаемо:
- нет ошибок
no such host(Traefik видитdockerapi_dockerproxy); - нет ошибок
port is missing(мы добавили явныйloadbalancer.server.port=80); - сертификаты по DNS-01 выпускаются при первом запросе на
https://traefik.YOU.DOMAIN.
7) Тестовый бэкенд (whoami) и правила
Чтобы быстро проверить роутинг:
version: "3.8"
services:
whoami:
image: traefik/whoami:latest
networks:
- proxy
deploy:
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.YOU.DOMAIN`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.whoami.tls.certresolver=le"
# Важно: явно указать порт backend-сервиса (он слушает 80)
- "traefik.http.services.whoami.loadbalancer.server.port=80"
networks:
proxy:
external: true
Деплой:
docker stack deploy -c whoami.yaml whoami
Проверьте https://whoami.YOU.DOMAIN — должен отвечать JSON-ом.
8) Разбор граблей и как мы их лечили
8.1 «no such host» у Traefik (providers.swarm)
Симптом: в логах Traefik бесконечно:
error during connect: Get "http://dockerapi_dockerproxy:2375/...": dial tcp: lookup dockerapi_dockerproxy on 127.0.0.11:53: no such host
Суть: контейнер Traefik не видит Overlay-DNS имени сервиса. Причин несколько, но типовые:
-
Контейнер не подключён к нужной overlay-сети. Проверка:
docker ps --filter label=com.docker.swarm.service.name=traefik_traefik docker inspect <CONTAINER_ID> --format '{{json .NetworkSettings.Networks}}'Должны быть оба:
proxyиdockerapi. Лечение:docker service update --network-add dockerapi traefik_traefik docker service update --force traefik_traefik -
Firewall мешает overlay-госсипу и VXLAN. Даже если порты формально «open», на edge мог стоять UFW поверх nftables и ломать порядок правил.
- Решение: выключить UFW, оставить CrowdSec nftables как единственный «дирижёр».
- Убедиться, что 7946/TCP+UDP и 4789/UDP разрешены между edge и всеми менеджерами в обе стороны.
-
Сломан локальный DNS в контейнере из-за
search-домена. Ранее стоял Netbird, который добавлял кresolv.confsearch netbird.selfhosted. В итоге запросы пытались резолвиться какdockerapi_dockerproxy.netbird.selfhosted.- Решение: удалить Netbird (пакеты, конфиги, WireGuard-интерфейс), вернуть
systemd-resolvedк нормальному виду, перезапустить Docker и сервисы.
- Решение: удалить Netbird (пакеты, конфиги, WireGuard-интерфейс), вернуть
-
Непоследовательность состояния сети. Иногда помогает «пере-вступить» edge-узел в Swarm (только для worker):
docker swarm leave docker swarm join --token PASTE_WORKER_TOKEN_HERE MANAGER_IP:2377Но в нашем случае достаточно было выключить UFW.
Как проверяли:
— из любого контейнера в сети dockerapi:
docker run --rm --network dockerapi alpine:3.20 sh -lc 'getent hosts dockerapi_dockerproxy; getent hosts tasks.dockerapi_dockerproxy'
— из самого контейнера Traefik:
docker ps --filter label=com.docker.swarm.service.name=traefik_traefik
docker exec -it <CONTAINER_ID> sh -lc 'getent hosts dockerapi_dockerproxy; wget -qO- http://dockerapi_dockerproxy:2375/_ping || true'
Ожидаем IP и OK.
8.2 «port is missing» у Traefik
Симптом: регулярная ошибка:
providerName=swarm error: service "traefik-traefik" error: port is missing
Суть: мы «включили» сам Traefik как сервис через traefik.enable=true, указали роутер на api@internal, но не объявили порт backend-сервиса. Traefik требует это даже для self-service (иначе он не может смоделировать LB).
Лечение: добавить лейбл:
- "traefik.http.services.traefik.loadbalancer.server.port=80"
После обновления ошибка исчезает.
9) CrowdSec, nftables и Docker
- Один «главный» firewall. Если используете
crowdsec-firewall-bouncer-nftables, UFW должен быть выключен, либо аккуратно интегрирован в nftables с приоритетами. - Порты Swarm должны быть «allow» до deny-цепочек.
- Docker создаёт свои таблицы/цепочки; не ломайте их.
Проверки:
nft list ruleset | less
journalctl -u crowdsec-firewall-bouncer -n 200 --no-pager
10) Короткий чеклист «здоровья» кластера
-
Менеджеры:
docker node ls— все 3 в
Ready, одинLeader, двоеReachable. -
Edge-узел — worker с меткой:
docker node inspect EDGE-HOST --format '{{json .Spec.Labels}}'— есть
node.labels.edge=true. -
Сети:
docker network ls docker network inspect proxy docker network inspect dockerapi— обе
overlay,dockerapi=Internal: true. -
docker-socket-proxy:
docker service ls | grep dockerproxy docker run --rm --network dockerapi curlimages/curl:8.8.0 -s http://dockerapi_dockerproxy:2375/_ping—
OK. -
Traefik:
docker service ls | grep traefik docker service logs -f traefik_traefik— нет
no such host, нетport is missing. -
ACME: — первый заход на
https://traefik.YOU.DOMAIN→ в логах DNS-01, выпуск сертификата. -
Прокси-тест:
curl -I https://whoami.YOU.DOMAIN—
HTTP/2 200и корректный ответ.
11) Почему отказались от Netbird (и когда mesh-VPN всё же уместен)
Внутри одного региона/провайдера:
- Overlay-сети Swarm + открытые межузловые порты обеспечивают связность.
- Mesh-VPN добавляет лишний слой (DNS-search, MTU, маршрутизацию), может ломать Swarm-DNS, усложняет разбор сетей.
Когда mesh-VPN пригодится:
- кросс-региональная/мультипровайдерная топология с NAT/CGNAT;
- потребность в сквозной L3 связности без пробросов портов;
- специфические SDS/репликации, где оверхед на VXLAN+WAN нежелателен.
Даже в этих случаях часто проще и стабильнее:
- прямой WireGuard site-to-site под конкретные подсети;
- для хранилищ — S3-совместимые решения вместо блочных/файловых по WAN;
- если файловая репликация — то с чёткими требованиями к MTU и расписанием.
12) Безопасность и операционные мелочи
- Не монтируйте
docker.sockв Traefik. Только через docker-socket-proxy с минимальным набором эндпоинтов. - Секреты храните в Swarm Secrets, а не в
environment. Так они не светятся вinspect. - Cloudflare trustedIPs держите актуальным списком.
- Экспорт метрик/логов Traefik — включайте accesslog, позже добавьте
format=jsonи отправляйте в централизованное хранилище. - Бэкапы
acme.json(томacme) — делайте, права 600 внутри контейнера Traefik выставляются автоматически.
13) Частые команды (без переменных)
Удалить стек:
docker stack rm traefik
docker stack rm dockerapi
Создать сети:
docker network create -d overlay --attachable proxy
docker network create -d overlay --attachable --internal dockerapi
Секреты:
printf '%s' 'PASTE_CF_DNS_API_TOKEN_HERE' | docker secret create cf_dns_api_token -
printf '%s\n' 'admin:$apr1$PASTE_HTPASSWD_HASH' | docker secret create traefik_users -
Деплой:
docker stack deploy -c /app/dockerapi/dockerapi-stack.yaml dockerapi
docker stack deploy -c /app/traefik/traefik-stack.yaml traefik
Проверки:
docker service ls
docker service ps dockerapi_dockerproxy
docker run --rm --network dockerapi curlimages/curl:8.8.0 -s http://dockerapi_dockerproxy:2375/_ping
docker service logs -f traefik_traefik
Внутрь контейнера Traefik (найдите ID командой ниже):
docker ps --filter label=com.docker.swarm.service.name=traefik_traefik
docker exec -it <CONTAINER_ID> sh
Overlay-DNS из тестового контейнера:
docker run --rm --network dockerapi alpine:3.20 sh -lc 'getent hosts dockerapi_dockerproxy; getent hosts tasks.dockerapi_dockerproxy'
Стоп UFW:
sudo ufw disable
Финал
В итоге мы получили чистую и стандартную схему:
- 3 manager узла с Raft-кворумом;
- 1 edge worker с меткой
edge=true, где слушают только 80/443; - Traefik v3 как ingress-контроллер в Swarm, без монтирования
docker.sock; - docker-socket-proxy на менеджерах, доступный Traefik в internal overlay;
- Cloudflare с orange cloud и ACME DNS-01 через минимальный токен;
- CrowdSec nftables как единственный firewall-«дирижёр», UFW выключен;
- Чёткие проверки Overlay-DNS и устранение причин ошибок
no such hostиport is missing.
С такой базой вы спокойно переносите существующие сервисы (SSO, CMS, приложения) на Swarm: каждому даёте labels, подключаете к proxy, прописываете router rules и (важно!) явный порт backend-сервиса. Всё остальное — уже творческая работа по маршрутизации, аутентификации и наблюдаемости.