Спецификация 22
Развёртывание сайта
Эта спецификация описывает, как сайт, собранный HERON, попадает на сервер и живёт там: контейнеры, скрипты обновления, CI, минимум для nginx, переезд со старого сайта и бэкап.
Она для того, кто ведёт сервер или репозиторий сайта. Специфика конкретного проекта живёт в документах самого проекта, а не здесь.
Простыми словами
Собранный сайт — это набор готовых страниц. Чтобы его видели все, он должен жить на сервере: компьютере, который всегда включён и отвечает посетителям. Эта страница описывает, как там живёт сайт на HERON.
Сайт переезжает как образ: готовые страницы, упакованные вместе с небольшим веб-сервером, как запечатанная коробка. Сервер запускает коробку; новая версия сайта — новая коробка, которая встаёт на место старой. Если что-то пошло не так, старая коробка никуда не делась и возвращается.
Пример. Запустить образ сайта на сервере и потом обновить его:
docker run -d --name my-site -p 80:8080 --restart unless-stopped ghcr.io/you/my-site:prod
docker pull ghcr.io/you/my-site:prod && docker rm -f my-site && docker run -d --name my-site -p 80:8080 --restart unless-stopped ghcr.io/you/my-site:prod
Первая команда запускает сайт. Вторая скачивает новую версию и меняет коробку. С git и GitHub Actions новый образ собирается сам при каждом push; подробнее — публикация через git.
Когда нужна эта страница. Когда вы настраиваете сервер, автоматизируете обновления, переносите старый сайт или продумываете резервные копии.
Статус на сегодня
Статус на 2026-10-04. Работающий сегодня путь выкладки — образ сайта:
scripts/site.sh <сайт> image|smoke|push <окружение>в репозитории HERON (поставка, выходы), дальше образ запускается как любой контейнер nginx. Схема «сборка на сервере» из разделов контейнеры и скрипты (composeсо сборщиком,install.sh,deploy.sh,deploy.conf) — целевая: этих скриптов в репозитории HERON пока нет. Разделы CI, nginx и переезд действуют для обоих путей.
На практике пошаговый гайд — сборка и выкладка, а способы хранения и публикации разобраны на странице публикация.
Что разворачиваем
Один сайт — одна папка и один репозиторий. В нём контент, данные, картинки, тема и конфиг. Кода HERON в нём нет: HERON приходит образом ghcr.io/<владелец>/heron:<версия>, версия объявлена в site.yaml.
Отсюда два следствия, определяющих весь деплой:
- обновление HERON — это смена одной строки в
site.yamlи пересборка, а не мерж чужого кода в репозиторий сайта; - откат HERON — возврат этой строки.
Контейнеры
Два, оба лёгкие. Третий (relay) появляется только на сайтах с формами.
Сборщик — образ HERON. Запускается разово, собирает сайт в dist/ и завершается. Между сборками не работает и ресурсов не ест. Собирать его на целевой машине не нужно: он тянется из реестра.
Веб — nginx, отдаёт dist/. Единственный постоянно работающий процесс. Образ nginxinc/nginx-unprivileged — мастер-процесс не от рута, слушает 8080; наружу смотрит прокси.
Базы данных нет. Приложения нет. Отдавать статику — единственное, что делает сайт в проде.
services:
build:
image: ghcr.io/OWNER/heron:0.1.0
profiles: ["build"] # не поднимается вместе с web
user: "${UID}:${GID}"
network_mode: "none" # сборке сеть не нужна
read_only: true
tmpfs: ["/tmp"]
cap_drop: ["ALL"]
security_opt: ["no-new-privileges:true"]
volumes:
- .:/site
command: ["build", "--strict"]
web:
image: nginxinc/nginx-unprivileged:1.27-alpine
restart: unless-stopped
volumes:
- ./dist:/usr/share/nginx/html:ro
# конфиг собирает HERON: языки, 404, карты 301 и 410 уже внутри
- ./dist/.heron-nginx.conf:/etc/nginx/conf.d/default.conf:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
interval: 30s
timeout: 3s
retries: 3
dist/ монтируется в nginx только на чтение. Сборщик пишет туда, веб только читает.
Сборщику не монтируется .env: секретов у сборки нет, она идёт без сети. Разбор модели безопасности — поставка, безопасность.
Два скрипта
Оба лежат в директории проекта на сервере, оттуда и запускаются. Стиль — set -e, понятные шаги, внятное падение.
install.sh — один раз при первой установке
- Проверяет наличие
dockerиdocker compose, версии. - Создаёт
dist/,cache/,logs/с нужными правами. - Копирует
.env.exampleв.env, если его нет, и останавливается с просьбой заполнить. Не запускается с пустыми секретами (актуально для сайтов с relay; на статическом сайте файл остаётся пустым). - Проверяет, что
deploy.confзаполнен. - Тянет образ HERON нужной версии из реестра.
- Первая сборка сайта.
- Поднимает nginx.
- Печатает, что дальше: проверить сайт, настроить проксирование, выпустить сертификат.
deploy.sh — при каждом обновлении
- Читает
deploy.confи.env. git pullнужной ветки.- Сверяет версию HERON из
site.yamlс локально доступным образом; при расхождении тянет нужный тег. - Собирает сайт во временный каталог
dist.new/, не трогая работающий. - Прогоняет
heron check --strict. Ошибки — остановка,dist/остаётся прежним. - Атомарная подмена:
dist→dist.old,dist.new→dist, перезагрузка nginx. - При падении на любом шаге — возврат
dist.oldи внятное сообщение. - Чистит
dist.oldпосле успешной проверки.
Ключевое: сборка идёт в сторону, подмена атомарна. Битый коммит не оставляет сайт наполовину обновлённым. Откат — это возврат предыдущего каталога, а не пересборка.
Два файла конфигурации
deploy.conf — параметры окружения, лежит в git:
REPO_BRANCH=main
SITE_DIR=/opt/site
DOMAIN=example.com
KEEP_RELEASES=3
.env — секреты, в git отсутствует, права 600. На статическом сайте пуст: секреты появляются с relay. Правила обращения — в контракте, раздел .env.
Разделение принципиальное: deploy.conf можно показать кому угодно, .env нельзя никому.
CI
По событию в репозитории сайта:
heron check --strict— падает на ошибках и на предупреждениях.heron buildво временный каталог.- Проверка карты редиректов: каждый старый URL отдаёт 301 на адрес, отвечающий 200.
- Lighthouse по собранному сайту, пороги задаёт сайт.
- Только если всё зелёное —
deploy.sh.
Тесты самого HERON гоняются в CI репозитория HERON, а не сайта: сайт потребляет готовый образ и не обязан знать, как тот собран.
Кэш нарезок в CI. Без него каждая сборка холодная: на сайте в сотню картинок это минуты процессорного времени на любую правку текста. Кэш лежит в .heron-cache/ рядом с папкой сборки и кладётся в кэш раннера как обычная папка:
- uses: actions/cache@v4
with:
path: .heron-cache
key: heron-media-${{ hashFiles('media/**', 'theme/theme.yaml') }}
restore-keys: heron-media-
Ключ включает тему не для красоты: widths, ratios и formats входят в хеш каждой записи, и правка theme.yaml обесценивает кэш целиком.
Кэш влияет только на скорость. Потерялся, протух, не восстановился — сборка просто будет холодной, результат тот же побайтово. Поэтому промахи кэша чинить не нужно, а build.workers в CI стоит прибить руками: ядра там делят с другими задачами.
Если сайт включил build.updated_from: git, истории нужно доехать до сборки. GitHub Actions клонирует репозиторий обрезанным — только последний коммит, — и тогда история формально есть, а дат в ней нет: HERON откатится на время файлов, а файлы после клона все датированы временем клона. Весь сайт объявит себя обновлённым сегодня, и так при каждой сборке. Лечится одной строкой:
- uses: actions/checkout@v7
with:
fetch-depth: 0
То же самое касается сборки из архива или из копии папки без .git: даты перейдут на время файлов. Если даты важны — история должна быть рядом с контентом в момент сборки.
Пункт 3 — не формальность. Это единственное, что защищает индекс при переезде со старой системы.
nginx
Минимум, который обязан быть в конфиге:
- отдача статики с длинным кэшем для
assets/иmedia/, коротким для HTML; - gzip и brotli;
- 301 из
redirect_fromи 410 изgoneотдаются без обращения к файлам. Карты уже вписаны в конфиг, который HERON собирает вместе с сайтом (dist/.heron-nginx.conf);redirects.mapиgone.mapвdist/— человекочитаемые копии; try_filesс завершающим слешем, единый канонический вид URL;/healthzдля healthcheck;- заголовки:
X-Content-Type-Options,Referrer-Policy,Content-Security-Policy, HSTS; - запрет исполнения чего-либо: сайт статический, PHP и любой обработчик отсутствуют как класс;
- закрыты dot-файлы.
CSP имеет смысл строгий: собственных скриптов единицы килобайт, внешних нет. script-src 'self' без исключений. На сайтах с формой добавляется connect-src до адреса relay — и больше ничего.
Переезд с существующего сайта
Порядок, при котором сайт не проседает. Конкретные правила редиректов — дело сайта, здесь только последовательность.
Подготовка. Новый сайт собирается и поднимается на служебном поддомене. Проверяется всё: страницы, языки, разметка, скорость.
Карта редиректов. Полный список старых адресов снимается с живого sitemap. Для каждого прописывается redirect_from в соответствующем md-файле; то, чего больше не существует, — архивы, служебные пути, дорвеи — пишется в gone в site.yaml, точными адресами или префиксами (site.yaml). Тест из CI, пункт 3, проходит на служебном домене.
Разница между 301 и 410 принципиальна: 301 говорит поисковику «адрес переехал, вот новый», 410 — «этого больше нет, выкинь из индекса». Служебные сущности старой системы — это второе, и притворяться, что они временно недоступны, смысла нет.
Пример. На старом сайте была страница /our-services/roofing.html; на новом это content/ru/services/roof.md. Во фронтматтере новой страницы записан старый адрес, и HERON пишет для него настоящий 301:
redirect_from:
- /our-services/roofing.html
Переключение. Прокси переводится на новый контейнер. Старый стек остаётся поднятым, но недоступным снаружи — на случай отката.
После. Sitemap переотправляется в Search Console. Неделю смотрим отчёт по ошибкам сканирования. Если тихо — старый стек гасится, каталоги архивируются.
Практический гайд по переезду с примерами — SEO и языки.
Бэкап
Резервировать нужно только то, что нельзя восстановить сборкой.
Нужно: репозиторий с контентом и картинками — он же и есть источник истины. .env. Конфиг nginx.
Не нужно: dist/ — пересобирается за секунды. cache/ — тем более. Образ HERON — он в реестре и прибит версией.
Сайт целиком восстанавливается из git одной командой. Это главное практическое преимущество перед системами, где восстановление означает разворачивание базы.
Что появится позже
Заложено в структуре, не реализуется сейчас:
heron-relay — третий контейнер на сайтах с формами: приём POST, валидация по схеме из site.yaml, антиспам, рассылка в почту, вебхук CRM и Telegram. Без базы; SQLite только как спул повторной отправки. Секреты из смонтированного .env, только в этот контейнер — nginx про них не знает.
Собственная аналитика — приём событий вместо внешнего счётчика. Отдельный эндпоинт relay, отдельное решение по хранению.
Админка — веб-редактор md-файлов с запуском пересборки. Правило, без которого её нельзя делать: админка правит файлы и коммитит их в git, а не пишет в базу. Иначе HERON превращается в ту самую систему, от которой сайты уходят.
Ни одно из трёх не требует переписывания того, что делается сейчас, — при условии, что границы из спецификации сборки и поставки соблюдены. Текущие планы — в плане развития.
Источник
Эта страница повторяет docs/spec/22-deploy.md из ветки prod репозитория HERON (документ 3 из 4; дата 2026-09-08, правка 2026-09-14). Первым обновляется репозиторий, эта страница — сразу следом. Последняя версия всегда здесь: 22-deploy.md на GitHub.