Top.Mail.Ru
OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

Wildcard DNS и превью‑стенды: поддомены на каждую ветку Git через Nginx map на VDS

Показываю рабочую схему превью‑стендов: одна VDS, wildcard DNS на dev‑домен, Nginx с map и скрипты, поднимающие приложения на уникальных портах для каждой ветки Git. Разберём санитаризацию имён, SSL, basic auth, конфиги map, перегенерацию и автоматическое удаление стендов.
Wildcard DNS и превью‑стенды: поддомены на каждую ветку Git через Nginx map на VDS

Превью‑стенды экономят часы на ревью фич: каждую ветку Git можно открыть живьём по отдельному поддомену, а не собирать инструкции для локального запуска. Самый быстрый способ — wildcard DNS на дев‑домен и маршрутизация через Nginx map на одной VDS. Получается гибко, прозрачно и без тяжёлых оркестраторов.

Идея и архитектура

Мы создаём поддомены вида branch.dev.example.com для всех веток Git. Wildcard DNS отправляет весь трафик на VDS с Nginx. Там map разбирает $host, определяет имя ветки и направляет запрос в соответствующий backend — процесс приложения, поднятый на уникальном порту. Как только ветка удаляется, процесс останавливается, запись в map исчезает, и поддомен больше не резолвится на сервис.

Такой подход хорош для preview environment под каждую задачу и для классических dev/stage инсталляций. Он не требует перезапуска Nginx на каждую ветку, если карту генерировать из темплейта и делать аккуратный reload без простоя.

Схема именования поддоменов

Главное — согласовать правила трансформации имён веток в DNS‑допустимые лейблы. Ограничения: 63 символа на лейбл, латиница, цифры и дефис; подчеркивания и слеши нельзя. Типовые правила:

  • Переводим в нижний регистр.
  • Заменяем недопустимые символы на дефис (/, _, ., пробелы, спецсимволы).
  • Урезаем до вменяемой длины (например, 32–40 символов) и добавляем хэш‑суффикс от исходного имени для уникальности.

Совет: фиксируйте одинаковые правила в CI и в серверных скриптах, чтобы URL и сервисы всегда сходились.

DNS: wildcard записи

На стороне DNS понадобится поддомен для превью — например, dev.example.com — и wildcard‑запись, которая указывает на ваш сервер. Если домена ещё нет, оформите его через регистрацию доменов. Минимальный набор:

  • dev.example.com — A/AAAA на IP вашей VDS.
  • *.dev.example.com — A/AAAA на тот же IP.

Если у вас есть отдельные пространства под stage или pr, добавьте и их wildcard. TTL для дев‑зон можно сделать поменьше (например, 60–300 секунд), чтобы быстрее обновлялись записи при перенастройках.

Nginx: извлекаем ветку из хоста и роутим через map

Базовый трюк: используем переменные и map, чтобы из $host выделить ветку, а затем выбрать backend. Начнём с простого варианта — backend в виде host:port в переменной. Для дев‑нагрузки это ок.

# /etc/nginx/conf.d/preview-map.conf
# 1) Извлекаем лейбл ветки из host
map $host $branch_label {
    ~^(?<sub>[^.]+)\.dev\.example\.com$ $sub;
    default "";
}

# 2) Соответствие лейбла и backend (порт или адрес)
# Этот блок генерируется скриптом при добавлении/удалении стенда
map $branch_label $branch_upstream {
    # примеры статических соответствий
    main 127.0.0.1:9001;
    stage 127.0.0.1:9002;
    default 127.0.0.1:9000; # страница-заглушка 404
}

# Для WebSocket/HTTP2 upgrade
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name ~^([^.]+)\.dev\.example\.com$;

    # Защита от случайных хостов
    if ($branch_label = '') { return 444; }

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 120s;
        proxy_connect_timeout 5s;
        proxy_pass http://$branch_upstream;
    }
}

Вариант с переменной в proxy_pass прост, но не поддерживает keepalive к upstream на уровне Nginx. Для превью‑нагрузки это не критично. Если хотите выжать максимум, генерируйте имена upstream‑блоков и маппите на них константами, а в proxy_pass используйте http://имя_upstream. Тогда возможен keepalive и общий пул соединений.

Страница‑заглушка и безопасное значение по умолчанию

Значение default в карте должно вести на явную заглушку, отдающую 404 и короткую подсказку. Так вы избежите случайной утечки трафика на не тот backend.

# /etc/nginx/conf.d/preview-404.conf
server {
    listen 127.0.0.1:9000;
    server_name _;
    location / { return 404; }
}

Фрагмент конфигурации Nginx map: извлечение имени ветки из $host и маршрутизация

Автоматизация генерации map

Ручками править карту неудобно. Создадим отдельный файл, который полностью перегенерируется скриптом при добавлении/удалении стенда. Включим его в основной конфиг Nginx:

# /etc/nginx/nginx.conf (фрагмент в http)
include /etc/nginx/maps/branches.map;

Содержимое /etc/nginx/maps/branches.map — полноценный блок map, чтобы Nginx мог atomically подхватить изменения:

# сгенерированный файл (не править вручную)
map $branch_label $branch_upstream {
    main 127.0.0.1:9001;
    stage 127.0.0.1:9002;
    default 127.0.0.1:9000;
}

Два скрипта обслуживают жизненный цикл: add‑preview и remove‑preview. Первый собирает приложение ветки, стартует процесс на свободном порту (или стабильном вычисляемом), добавляет соответствие в карту и делает nginx -s reload. Второй останавливает процесс и пересобирает карту без записи.

Выбираем порт: стабильно и без коллизий

Для предсказуемости удобно привязывать порт к имени ветки. Простейший вариант — базовый порт плюс хэш от лейбла. Например, 5000 + (crc32(label) mod 1000). Можно хранить соответствия в текстовом реестре, чтобы при коллизии выбрать первый свободный.

# пример грубого вычисления порта (bash + sha1sum + awk)
label="$1"
base=5000
span=1000
hash=$(printf "%s" "$label" | sha1sum | awk '{print $1}')
num=$((0x${hash:0:4} % span))
port=$((base + num))
echo "$port"

Для продвинутых сценариев храните JSON/INI с назначенными портами и проверяйте, занят ли порт, перед запуском.

Системный юнит для процессов превью

Удобно шаблонизировать запуск веток через systemd, чтобы единообразно стартовать/стопить. Шаблонный юнит с параметром — именем ветки (уже санитизированной):

# /etc/systemd/system/myapp-preview@.service
[Unit]
Description=MyApp preview for branch %i
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/myapp
Environment=BRANCH=%i
EnvironmentFile=-/opt/myapp/.env.preview
ExecStart=/opt/myapp/scripts/run-preview.sh %i
Restart=on-failure
RestartSec=2s

[Install]
WantedBy=multi-user.target

Скрипт run-preview.sh принимает лейбл ветки, вычисляет порт, экспортирует PORT и запускает приложение (например, PHP‑FPM pool, Node, Python, Go). В нём же можно писать health‑лог и PID.

# /opt/myapp/scripts/run-preview.sh
#!/usr/bin/env bash
set -euo pipefail
label="$1"
port=$(/opt/myapp/scripts/port-for.sh "$label")
export PORT="$port"
# Пример для Node.js
exec /usr/bin/node /opt/myapp/build/server.js

Скрипты: добавить и удалить превью‑стенд

Скрипт добавления: клонирует/обновляет код ветки, билдит, стартует systemd‑юнит и перегенерирует карту.

# /opt/myapp/scripts/add-preview.sh
#!/usr/bin/env bash
set -euo pipefail
raw_branch="$1"
# санитаризация в DNS‑лейбл
label=$(echo "$raw_branch" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g' | sed -E 's/^-+|-+$//g' | cut -c1-40)

# сборка
cd /opt/myapp
if [ -d repo ]; then
  cd repo && git fetch --all --prune && git checkout "$raw_branch" && git pull --ff-only
else
  git clone --origin origin /opt/git/remote.git repo
  cd repo && git checkout "$raw_branch"
fi
# ваш билд: npm ci && npm run build, composer install, и т.п.

# запуск превью
systemctl daemon-reload
systemctl enable --now myapp-preview@"$label"

# зарегистрировать backend
port=$(/opt/myapp/scripts/port-for.sh "$label")
/opt/myapp/scripts/rebuild-map.sh "$label" "127.0.0.1:$port"
nginx -t
nginx -s reload

echo "Ready: ${label}.dev.example.com"

Скрипт удаления: останавливает юнит, удаляет соответствие и перезагружает Nginx.

# /opt/myapp/scripts/remove-preview.sh
#!/usr/bin/env bash
set -euo pipefail
raw_branch="$1"
label=$(echo "$raw_branch" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g' | sed -E 's/^-+|-+$//g' | cut -c1-40)

systemctl disable --now myapp-preview@"$label" || true
/opt/myapp/scripts/rebuild-map.sh --remove "$label"
nginx -t
nginx -s reload

Генератор карты читает текущие пары лейбл→backend из простого каталога или KV‑хранилища и выплёвывает полный блок map:

# /opt/myapp/scripts/rebuild-map.sh (пример с простым хранилищем)
#!/usr/bin/env bash
set -euo pipefail
state_dir="/opt/myapp/state"
map_file="/etc/nginx/maps/branches.map"
mkdir -p "$state_dir"

if [ "${1:-}" = "--remove" ]; then
  label="$2"
  rm -f "$state_dir/$label.upstream"
else
  label="$1"
  upstream="$2"
  echo "$upstream" > "$state_dir/$label.upstream"
fi

echo "map \$branch_label \$branch_upstream {" > "$map_file.tmp"
for f in "$state_dir"/*.upstream; do
  [ -e "$f" ] || continue
  l=$(basename "$f" .upstream)
  u=$(cat "$f")
  echo "    $l $u;" >> "$map_file.tmp"
done
echo "    default 127.0.0.1:9000;" >> "$map_file.tmp"
echo "}" >> "$map_file.tmp"

mv "$map_file.tmp" "$map_file"

Интеграция с Git и CI

Подключите скрипты к событиям в вашей цепочке — при пуше ветки или открытии Pull Request вызывайте add-preview.sh, при закрытии PR или удалении ветки — remove-preview.sh. Обязательно передавайте исходное имя ветки для корректной санитаризации. Храните соответствие «исходная ветка → лейбл» в артефактах CI или в вашем state_dir, если нужно строгое соответствие URL → исходное имя.

Шаблонный systemd‑юнит для превью‑экземпляров и сопоставление портов веткам

SSL/TLS для wildcard поддоменов

Для удобства используйте wildcard‑сертификат на *.dev.example.com. Так один сертификат накроет все превью‑поддомены, и пользователи смогут смотреть стенды по HTTPS без предупреждений. Выпускайте его через наши SSL-сертификаты. Об автоматизации через DNS‑01 мы подробно писали тут: как выпускать wildcard SSL по DNS‑01.

Базовая защита превью‑стендов

Обычно превью не предназначены для публичного индекса. Достаточно базовой аутентификации и запрета индексации:

# во внутреннем server‑блоке превью
location / {
    auth_basic "Preview";
    auth_basic_user_file /etc/nginx/htpasswd.preview;
    add_header X-Robots-Tag "noindex, nofollow" always;
    proxy_pass http://$branch_upstream;
}

Файл паролей можно создать через htpasswd из пакета apache2-utils. Для автоматизации положите его рядом с конфигурацией и управляйте пользователями из CI.

Отладка и проверка

  • Проверяйте DNS: dig +short feature.dev.example.com должен показывать IP VDS.
  • Тестируйте Nginx локально: curl -H "Host: feature.dev.example.com" http://127.0.0.1/.
  • Смотрите error.log при 502/404 — чаще всего карта не содержит нужной записи или сервис не запущен.

Особенности и частые грабли

  • nginx map и переменные: переменная в proxy_pass отключает keepalive к upstream. Для превью это нормально; для тяжёлых стендов генерируйте именованные upstream.
  • Санитаризация имён: не все ветки подходят для DNS. Заменяйте запрещённые символы и ограничивайте длину. Старайтесь избегать двойных дефисов в конце/начале.
  • TTL DNS: при большом TTL изменения могут «ехать» минутами из‑за кэшей резолверов.
  • Порты и firewall: backend слушает только 127.0.0.1, наружу открыт лишь 80/443 с Nginx.
  • WebSocket: не забудьте о Upgrade/Connection и таймаутах.
  • Статика и кэш: для SPA/API превью достаточно проксирования; дополнительные кэши обычно не нужны, чтобы видеть свежие сборки.

Альтернативы и расширения

Если поддомены не вариант, можно реализовать routing по пути (/preview/branch/) — это проще в DNS, но сложнее в приложении из‑за абсолютных URL и куки. Ещё вариант — запускать превью в контейнерах и публиковать их через общий reverse proxy; Nginx map остаётся центром маршрутизации.

Пошаговый план внедрения

  1. Завести дев‑домен и настроить wildcard DNS на IP VDS.
  2. Установить Nginx, включить конфиги map и сервер для *.dev.example.com, поднять заглушку 404.
  3. Сделать шаблонный systemd‑юнит для приложения и скрипты add/remove.
  4. Интегрировать скрипты в CI: on push/open PR → add-preview, on close/delete → remove-preview.
  5. Подключить wildcard TLS, включить basic auth и X-Robots-Tag.
  6. Обкатать на тестовых ветках, замерить время развёртывания и стабилизировать пайплайн.

Итоги

Комбинация wildcard DNS, Nginx map и простых скриптов даёт предсказуемые превью‑стенды: каждый коммит в ветке быстро получает собственный поддомен, а маршрутизация остаётся прозрачной и управляемой. Такой подход масштабируется на десятки веток без тяжёлых инструментов, а при росте требований легко расширяется до именованных upstream, контейнеров и дополнительных политик безопасности. Для команд разработки это быстрый способ ускорить ревью и сократить «не воспроизводится у меня локально» — всё открывается по стабильному URL.

Поделиться статьей

Вам будет интересно

Hardening сервисов на VDS: sandbox опции systemd — ProtectSystem, PrivateTmp, CapabilityBoundingSet OpenAI Статья написана AI Fastfox

Hardening сервисов на VDS: sandbox опции systemd — ProtectSystem, PrivateTmp, CapabilityBoundingSet

Как ограничить права Linux‑сервисов на VDS с помощью sandbox‑опций systemd. Разбираем ProtectSystem, PrivateTmp, CapabilityBoundin ...
Память на малом VDS без сюрпризов: swap, zram, vm.overcommit и OOM‑killer на практике OpenAI Статья написана AI Fastfox

Память на малом VDS без сюрпризов: swap, zram, vm.overcommit и OOM‑killer на практике

Малый VDS часто упирается в память: PHP‑FPM, базы, кэш и воркеры делят считанные гигабайты. Ошибка Killed в логах и зависания — пр ...
Умный кэш Nginx для API и внешних бэкендов: proxy_cache, stale‑while‑revalidate и cache lock OpenAI Статья написана AI Fastfox

Умный кэш Nginx для API и внешних бэкендов: proxy_cache, stale‑while‑revalidate и cache lock

Как снизить латентность и нагрузку на внешние бэкенды, не ломая логику API? Разберём боевые паттерны Nginx: proxy_cache, ключи и T ...