Cloudflare DNS pool balancer
Лёгкий контейнер на Alpine: периодически проверяет доступность бэкендов (ping или HTTP), синхронизирует записи A / AAAA для одного DNS-имени (пул) в Cloudflare через API Token, пишет цветные логи и опционально отдаёт HTML-страницу статуса с ограничением по IP.
Оглавление
- Как это работает
- Требования
- Сборка образа (Linux / bash)
- Быстрый старт
- Переменные окружения
- Cloudflare: API Token
- Примеры запуска
- Docker Compose
- Веб-интерфейс статуса и whitelist
- Логи
- Ограничения
- Устранение неполадок
- Сборка в CI (Gitea Actions)
- Безопасность
- Windows и WSL2
Как это работает
- Для каждой цели из
CHECK_TARGETSопределяется IP (прямой адрес, резолв имени или хоста из URL). - Выполняется проверка: ICMP ping к IP или curl по URL (см.
CHECK_MODE). - IP считается желательным в пуле, только если все цели, которые резолвятся в этот IP, прошли проверку.
- Читаются текущие записи A/AAAA с именем
POOL_DOMAINв Cloudflare. - Лишние записи удаляются, недостающие создаются (TTL и proxied из переменных окружения).
Два разных смысла curl:
- Проверка бэкендов (
CHECK_MODE=curl) — запросы к URL из целей / шаблонуCURL_URL_TEMPLATE. - Порт
STATUS_HTTP_PORT— только встроенная страница статуса внутри контейнера, не health-check.
CHECK_TARGETS: не только «локальные» адреса
В инструкции в примерах часто встречаются частные IP — это лишь пример. Формат тот же для любых целей, которые контейнер реально может проверить:
| Тип | Примеры |
|---|---|
| Частные IP | 10.0.0.1, 192.168.1.10, 172.16.0.5 |
| Публичные (глобальные) IP | 203.0.113.50, адреса VPS/анонсеров |
| Внутренние FQDN | backend.prod.local, node1.dc.company.internal |
| Публичные домены | api.example.com, origin.example.org |
URL (режим curl) |
https://api.example.com/health, http://203.0.113.1:8080/ping |
Имя резолвится через DNS из контейнера (dig); для сопоставления с пулом Cloudflare используется полученный A/AAAA. Проверка — обычный ping к IP или curl к URL.
Единственное условие: из сети контейнера до цели должен доходить трафик (маршрутизация, файрвол, docker network, VPN и т.д.). Публичные хосты в интернете обычно доступны из стандартного bridge; частные адреса за NAT без проброса/маршрута — нет, пока не настроите сеть.
Требования
- Docker Engine на Linux (или эквивалент с Linux-контейнерами).
- У контейнера должен быть доступ в интернет (Cloudflare API, ping/curl к бэкендам).
- Для ping из пользователя
nobodyв образе выставленcap_net_rawна/usr/bin/ping. Если ping не работает, запускайте с--cap-add=NET_RAW(вdocker-compose.ymlэто уже указано).
Сборка образа (Linux / bash)
Из корня репозитория:
cd /path/to/cloudflare_balancer
docker build -t cloudflare-balancer:latest .
Проверка локально (без реальных секретов контейнер сразу завершится с ошибкой — это нормально):
docker build -t cloudflare-balancer:latest . && echo "сборка OK"
Быстрый старт
Минимально нужны: POOL_DOMAIN, CHECK_TARGETS, CLOUDFLARE_API_TOKEN.
Рекомендуется передавать секреты через --env-file:
cp .env.example .env
# отредактируйте .env
docker run --rm \
--cap-add=NET_RAW \
--env-file .env \
cloudflare-balancer:latest
Переменные окружения
| Переменная | Обязательно | По умолчанию | Описание |
|---|---|---|---|
POOL_DOMAIN |
да | — | DNS-имя пула (все A/AAAA с этим именем управляются скриптом). |
CHECK_TARGETS |
да | — | Список через запятую: любые достижимые цели — частные/публичные IP, внутренние или глобальные имена, либо http(s)://... (подробнее — подраздел «CHECK_TARGETS: не только „локальные“ адреса» выше). |
CLOUDFLARE_API_TOKEN |
да* | — | Bearer-токен Cloudflare. |
CLOUDFLARE_API_KEY |
нет | — | Алиас для токена (если не задан CLOUDFLARE_API_TOKEN). |
CLOUDFLARE_ZONE_ID |
нет | — | ID зоны; если пусто — поиск по имени зоны. |
CLOUDFLARE_ZONE_NAME |
нет | — | Имя зоны (apex), например example.com; если пусто — эвристика: две последние метки POOL_DOMAIN. |
CHECK_INTERVAL_SEC |
нет | 30 |
Пауза между циклами, сек. |
CHECK_MODE |
нет | ping |
ping или curl. |
PING_COUNT |
нет | 2 |
Число ICMP-запросов. |
PING_TIMEOUT_SEC |
нет | 2 |
Таймаут ping (см. iputils-ping). |
CURL_URL_TEMPLATE |
нет | http://%s/ |
Для не-URL целей: один плейсхолдер %s заменяется на цель. |
CURL_MAX_TIME_SEC |
нет | 5 |
--max-time для curl. |
CURL_CONNECT_TIMEOUT_SEC |
нет | 3 |
--connect-timeout. |
CURL_OK_MIN |
нет | 200 |
Минимальный допустимый HTTP-код. |
CURL_OK_MAX |
нет | 399 |
Максимальный допустимый HTTP-код. |
DEFAULT_TTL |
нет | 300 |
TTL при создании записи (для прокси-записей Cloudflare сам использует авто-TTL). |
DEFAULT_PROXIED |
нет | false |
true / false — оранжевое облако. |
ENABLE_STATUS_HTTP |
нет | 0 |
1 — включить HTTP-страницу статуса. |
STATUS_HTTP_PORT |
нет | 8080 |
Порт внутри контейнера для страницы статуса. |
STATUS_HTTP_ALLOW_IPS |
нет | (пусто) | Whitelist: IP и IPv4 CIDR через запятую. Пусто = слушать только 127.0.0.1. |
* Обязателен токен или алиас.
Важно: эвристика зоны по двум последним меткам не подходит для всех публичных суффиксов (например некоторые зоны второго уровня). В сомнениях задайте
CLOUDFLARE_ZONE_IDилиCLOUDFLARE_ZONE_NAME.
Cloudflare: API Token
- Cloudflare Dashboard → My Profile → API Tokens → Create Token.
- Шаблон Edit zone DNS или кастомный минимум:
- Zone — DNS — Edit
- Zone — Zone — Read
- Ограничить конкретной зоной (рекомендуется).
- Скопируйте токен в
.envкакCLOUDFLARE_API_TOKEN.
Официальная документация API: Cloudflare API.
Примеры запуска
Только ping, без UI
docker run --rm --cap-add=NET_RAW \
-e POOL_DOMAIN=app.example.com \
-e CHECK_TARGETS='10.0.0.1,10.0.0.2' \
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
-e CHECK_INTERVAL_SEC=20 \
cloudflare-balancer:latest
Режим curl и шаблон URL
docker run --rm --cap-add=NET_RAW \
-e POOL_DOMAIN=app.example.com \
-e CHECK_TARGETS='backend1.internal,backend2.internal' \
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
-e CHECK_MODE=curl \
-e CURL_URL_TEMPLATE='http://%s:8080/health' \
cloudflare-balancer:latest
Полный URL как цель
docker run --rm --cap-add=NET_RAW \
-e POOL_DOMAIN=app.example.com \
-e CHECK_TARGETS='https://node1.example.com/health' \
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
-e CHECK_MODE=curl \
cloudflare-balancer:latest
Смешанные цели: частные IP, публичный домен, глобальный IP
docker run --rm --cap-add=NET_RAW \
-e POOL_DOMAIN=app.example.com \
-e CHECK_TARGETS='10.0.0.1,origin.example.com,203.0.113.10' \
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
cloudflare-balancer:latest
(Замените 203.0.113.10 на реальный публичный IP бэкенда; origin.example.com — на любой FQDN, который резолвится и отвечает на ping из контейнера.)
Веб-статус + проброс порта + whitelist
При доступе с хоста Docker клиентский IP в контейнере часто совпадает с адресом шлюза bridge (часто 172.17.0.1 или подсеть 172.17.0.0/16). Добавьте её или конкретный IP в whitelist.
docker run --rm --cap-add=NET_RAW \
-p 8080:8080 \
-e POOL_DOMAIN=app.example.com \
-e CHECK_TARGETS='10.0.0.1' \
-e CLOUDFLARE_API_TOKEN="$CLOUDFLARE_API_TOKEN" \
-e ENABLE_STATUS_HTTP=1 \
-e STATUS_HTTP_PORT=8080 \
-e STATUS_HTTP_ALLOW_IPS='127.0.0.1,172.17.0.0/16,10.0.0.0/8' \
cloudflare-balancer:latest
Откройте в браузере: http://127.0.0.1:8080/ (если whitelist это разрешает).
Docker Compose
cp .env.example .env
# заполните CLOUDFLARE_API_TOKEN и остальное
docker compose build
docker compose up -d
docker compose logs -f
Файл docker-compose.yml подключает .env и добавляет NET_RAW для ping.
Веб-интерфейс статуса и whitelist
STATUS_HTTP_PORT— порт HTTP-сервера статуса, не имеет отношения кCHECK_MODE=curl.- Если
STATUS_HTTP_ALLOW_IPSпустой —socatслушает 127.0.0.1 (доступ с хоста через-pбез NAT к loopback контейнера обычно не попадёт на 127.0.0.1 внутри контейнера). Для просмотра со стороны хоста задайте whitelist и0.0.0.0-bind (это делается автоматически при непустом whitelist). - В whitelist поддерживаются точные IPv4 и IPv4 CIDR; для IPv6 — в основном точное совпадение (CIDR для v6 в обработчике минимальный).
Логи
docker logs -f <container_id>
В логах используются ANSI-цвета (удобно в терминале). Блоки: заголовок цикла, строки OK/FAIL по целям, действия DELETE/POST в API.
Ограничения
- Одна строка в
CHECK_TARGETSс запятой внутри URL может сломать разбор — избегайте запятых в целях или используйте только один хост на цель. - Несколько имён, резолвящихся в один IP, считаются одним бэкендом: IP остаётся в пуле только если все такие проверки успешны.
- Записи A/AAAA для
POOL_DOMAIN, которые не соответствуют IP из текущего резолва целей, не удаляются (в лог и HTML выводится предупреждение «вне списка»).
Устранение неполадок
| Симптом | Что проверить |
|---|---|
| Ошибка авторизации Cloudflare | Токен, срок, зона в области действия токена. |
| «Зона не найдена» | Задайте CLOUDFLARE_ZONE_ID или корректный CLOUDFLARE_ZONE_NAME. |
| Все ping FAIL | NET_RAW / cap_add; маршрутизация сети контейнера до бэкендов; ICMP может быть запрещён файрволом. |
| curl всегда FAIL | URL, TLS, коды ответа (CURL_OK_MIN / CURL_OK_MAX), таймауты. |
| UI отдаёт 403 | Whitelist: добавьте IP шлюза Docker или вашу подсеть. |
| После старта пул «пустой» | При всех FAIL записи удаляются — проверьте доступность целей и корректность IP. |
Сборка в CI (Gitea Actions)
- На сервере Gitea включите Actions и подключите runner (документация).
- Workflow: .gitea/workflows/docker.yml — Buildx,
docker/login-action,docker/build-push-action; пути и метаданные из контекстаgitea.*(repository,actor,sha,ref_name,server_url).
Авторизация в registry
- Отдельный
ACTIONS_PATне нужен: вdocker loginиспользуется встроенныйgitea.token(токен текущего запуска workflow) иgitea.actor. У job заданоpermissions: packages: write— этого достаточно для push в Container Registry на том же Gitea (при несовместимости вашей версии см. документацию Gitea Actions).
Секрет для обновления контейнера (опционально)
CONTAINER_UPDATE_URL— полный URL webhook после успешного push (например ссылка Redeploy в Portainer, другой оркестратор). Workflow делаетcurl(GET) по этому URL; если секрет пустой, шаг только пишет в лог и выходит. При необходимости другого метода (POST) измените шаг вdocker.yml.
Переменные в workflow
env.REGISTRYв началеdocker.yml— хост registry без схемы (https://), как дляdocker pull(в файле задан примерgit.shts.su; замените при переносе на другой инстанс).IMAGE_REPO:${{ gitea.repository }}— полный путь образа:${REGISTRY}/${{ gitea.repository }}.
Триггеры: push по всем веткам и тегам, workflow_dispatch.
Теги: latest, «безопасное» имя ветки/тега (/ → -), sha-<12 hex>.
Job: permissions: contents: read, packages: write.
Безопасность
- Не коммитьте
.envи реальные токены. - Минимизируйте права API Token одной зоной.
- Не публикуйте порт статуса в интернет без whitelist и без понимания сетевой модели Docker.
Windows и WSL2
Основные команды из этой инструкции рассчитаны на bash под Linux. На Windows удобнее запускать Docker внутри WSL2 и выполнять те же docker build / docker run из Ubuntu (или другого дистрибутива в WSL).
Лицензия
Проект в репозитории пользователя — при необходимости добавьте файл LICENSE отдельно.