Задача проста: исходники статического сайта в Git, каждый push в основную ветку автоматически собирает проект и заливает на виртуальный хостинг. В деталях всё сложнее: нужно аккуратно настроить SSH-ключи, подобрать флаги rsync, продумать кэширование и уметь откатываться. Ниже — практический конспект с примерами рабочих конфигов.
Зачем статика и почему rsync на виртуальном хостинге
Статический сайт на виртуальном хостинге — минимальные требования к серверу, высокая скорость, низкие накладные расходы и почти нулевая поверхность атаки. На стороне хостинга нужен лишь веб-сервер и доступ по SSH. Для доставки артефактов идеально подходит rsync: он передаёт только изменения, экономит трафик и время деплоя, а ещё даёт гибкую политику удаления старых файлов.
Ключевое преимущество rsync — дифференциальная передача и точный контроль над конечным деревом файлов через
--delete.
Целевой процесс
Соберём такой поток:
- Вы коммитите изменения и пушите в
main. - GitHub Actions запускает сборку статического сайта.
- Готовая папка
dist(илиpublic) отправляется на хостинг черезrsyncпо SSH. - Опционально — выкладка через промежуточный каталог для минимизации «дрожания» сайта во время обновления.
Подготовка площадки на хостинге
Перед первым деплоем проверьте базовое:
- Доступ по SSH: логин, хост, порт, домашний каталог, документ‑рут (например,
~/public_html). - Наличие
rsyncна сервере:ssh -p 22 user@host rsync --versionдолжен отработать. - Достаточно прав на запись в целевой каталог и создание подкаталогов (например,
~/releases,~/stage).
Создайте на стороне хостинга финальный путь для артефактов, если его нет:
ssh -p 22 user@host "mkdir -p ~/public_html"

Подготовка репозитория
Распределите структуру проекта так, чтобы артефакты сборки были в одной папке: dist, build, public — не критично как называется, главное — единообразие. Добавьте в .gitignore артефакты и кэш:
dist/
.cache/
node_modules/
Для стабильного окружения фиксируйте версии инструментов: package-lock.json для Node‑проектов или определённую версию Hugo/Eleventy/11ty. При необходимости добавьте .gitattributes и включите нормализацию концов строк.
Workflow GitHub Actions: Node‑статик (Vite/Eleventy/Next static export)
Базовый workflow, который собирает проект на Node 20 и деплоит готовую папку dist через rsync. Секреты укажем в настройках репозитория: SSH_PRIVATE_KEY, DEPLOY_HOST, DEPLOY_PORT (обычно 22), DEPLOY_USER, DEPLOY_PATH (например, /home/user/public_html).
name: build-and-deploy
on:
push:
branches: [ "main" ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install deps
run: npm ci
- name: Build
run: npm run build
- name: Prepare SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -p "${{ secrets.DEPLOY_PORT }}" "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts
- name: Create remote dir if missing
run: ssh -p "${{ secrets.DEPLOY_PORT }}" "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" "mkdir -p \"${{ secrets.DEPLOY_PATH }}\""
- name: Rsync deploy
run: rsync -az --delete --delay-updates --omit-dir-times --no-perms --no-owner --no-group --human-readable --stats -e "ssh -p ${{ secrets.DEPLOY_PORT }}" ./dist/ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/"
Комментарии к ключам:
-aархивный режим (сохраняет время и рекурсивно копирует),-zсжатие,--deleteудаляет файлы, которых нет в исходном каталоге,--delay-updatesсначала загружает во временные файлы и потом заменяет — меньше «дрожания» при обновлениях,--omit-dir-timesизбегает попытки установить время на директориях (иногда запрещено на шаред‑хостинге),--no-perms --no-owner --no-groupне пытается менять права/владельца,--human-readable --statsудобная статистика.
Обратите внимание на слэш в конце ./dist/: с ним rsync переносит содержимое каталога, а не сам каталог.
Альтернатива: Hugo
Если вы на Hugo, используйте подобную схему: сначала установка конкретной версии Hugo, затем hugo --minify и тот же шаг с rsync. Суть остаётся прежней — деплоим папку public.
- name: Install Hugo
run: sudo apt-get update && sudo apt-get install -y hugo
- name: Build
run: hugo --minify
- name: Rsync deploy
run: rsync -az --delete --delay-updates -e "ssh -p ${{ secrets.DEPLOY_PORT }}" ./public/ "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/"
Минимизация простоя: два безопасных подхода
Полностью атомарная выкладка через переключение симлинка — идеальна, но на виртуальном хостинге не всегда можно сменить целевой документ‑рут. Ниже два подхода, которые обычно доступны.
1) Отложенная замена файлов (--delay-updates)
Мы уже включили этот флаг: rsync сначала зальёт во временные файлы, а затем массово подменит целевые. Для большинства статических сайтов этого достаточно, особенно если HTML небольшой, а ресурсы отдаются долго закешированными.
2) Промежуточный каталог на сервере
Идея: выгрузить сборку в ~/stage и одной дальней командой синхронизировать на боевой каталог. Две короткие операции уменьшают окно несогласованности.
# 1) выгружаем свежую сборку на сервер в stage
rsync -az --delete -e "ssh -p $PORT" ./dist/ user@host:~/stage/
# 2) на сервере быстро синхронизируем stage -> public_html
ssh -p $PORT user@host "rsync -a --delete --omit-dir-times --no-perms --no-owner --no-group ~/stage/ ~/public_html/"
Можно добавить каталог ~/releases/timestamp и хранить несколько версий для откатов — подробно об этом ниже. Отдельно рекомендую материал Миграция сайта без простоя — пригодится при переносе на новую площадку.
Кэширование: быстрые отлеты из браузера и CDN
Статика раскрывается на полную только вместе с грамотным кэшированием. Принцип прост: HTML — короткий TTL (или вообще без кэша), а ассеты с хешем в имени — на год с флагом immutable. При этом новый релиз меняет имя файла (fingerprint) — и браузеры моментально подхватывают его.
Апач (.htaccess)
На виртуальном хостинге часто доступен Apache с .htaccess. Пример минимальной настройки для ассетов и HTML. Не забудьте экранировать угловые скобки в конфиге, как ниже.
<IfModule mod_headers.c>
<FilesMatch "\.(css|js|mjs|svg|ico|png|jpg|jpeg|gif|webp|avif|woff|woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "\.(html)$">
Header set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
</FilesMatch>
</IfModule>
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/html "access"
ExpiresDefault "access plus 1 year"
</IfModule>
Nginx (для тех, у кого есть доступ к серверному конфигу)
Если площадка отдаётся Nginx и вы управляете конфигом (или через панель есть пользовательские директивы), логика та же. Полный доступ чаще встречается на VDS:
location ~* \.(css|js|mjs|svg|ico|png|jpg|jpeg|gif|webp|avif|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~* \.(html)$ {
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0";
}
В любом случае используйте fingerprinting: в процессе сборки добавляйте хеш содержимого в имя файла (app.4f9c1.css). Современные сборщики (Vite, webpack, Hugo) умеют это «из коробки». Не забудьте HTTPS: подключите SSL для корректного кэширования и безопасности.
Безопасность: ключи, known_hosts и права
Секреты храните только в Secrets репозитория/организации. Лучше генерировать отдельный деплой‑ключ и выдать ему доступ только к нужной учётке на хостинге. В раннере:
- кладём приватный ключ в
~/.ssh/id_ed25519с правами600; - добавляем ключ хоста в
known_hostsчерезssh-keyscan— так сохраняемStrictHostKeyChecking; - не передаём пароль в явном виде, не отключаем проверку ключа сервера опцией
-o StrictHostKeyChecking=no.
На стороне хостинга ограничьте права каталога деплоя только своим пользователем. Если доступно, включите двухфакторную авторизацию в панели, отделите пользовательские учётки «для людей» и «для CI».
Откаты: хранение нескольких релизов
Статика прекрасна тем, что «легендарный откат» — это просто переключение каталога. На виртуальном хостинге это можно реализовать без прав суперпользователя.
- Заливка в каталог релиза. Каждый билд выгружайте в
~/releases/<timestamp>. - Быстрое обновление боевого каталога. После заливки выполняйте локальную синхронизацию на сервере.
- Чистка старых версий. Держите 3–5 релизов, остальное удаляйте.
# 1) создание каталога релиза
ssh -p $PORT user@host "mkdir -p ~/releases/2025-01-01_120000"
# 2) выгрузка в релиз
rsync -az --delete -e "ssh -p $PORT" ./dist/ user@host:~/releases/2025-01-01_120000/
# 3) быстрое обновление продового каталога
ssh -p $PORT user@host "rsync -a --delete --omit-dir-times --no-perms --no-owner --no-group ~/releases/2025-01-01_120000/ ~/public_html/"
# 4) чистка старых релизов (оставляем 5)
ssh -p $PORT user@host "ls -1dt ~/releases/* | tail -n +6 | xargs -r rm -rf"
Такой подход обеспечивает предсказуемый откат: просто повторите шаг 3 с прежним каталогом релиза. Дополнительно про бэкапы и транспорт — в статье Резервные копии и rsync: практики.
Диагностика и типичные ловушки rsync
- Права и владельцы. На шаред‑хостинге часто нельзя менять владельца/группу. Отключайте это флагами
--no-owner --no-group, а также--no-perms, если права выставляются на стороне сервера. - Временные метки директорий. Некоторые FS запрещают менять их время — используйте
--omit-dir-times. - Пустой деплой. Проверьте, что путь назначения существует и у вас есть права на запись. Перед первым деплоем выполните
ssh user@host "mkdir -p /path". - Слэш в конце пути. Убедитесь, что понимаете семантику
./dist/vs./dist: первый копирует содержимое, второй создаст вложенную папку. - Трим старых файлов. Если вы переименовали папку ассетов, но не включили
--delete, «мусор» останется и может продолжить отдаваться по прямым URL. Включайте--deleteи поддерживайте чистоту. - Проверка перед выкладкой. Запускайте
rsyncс--dry-run --itemize-changes, когда меняете флаги, чтобы увидеть, что именно будет обновлено.
Производительность сборки в CI
Чтобы ускорить пайплайн, включайте кэш менеджера пакетов: для Node — cache: 'npm' в шаге actions/setup-node. Фиксируйте версии, избегайте установок глобальных инструментов, держите сборку идемпотентной. Следите, чтобы в артефакты не попадали лишние мегабайты (исключайте исходники, карты, черновики и т. п., если они не нужны в проде).
Контроль кэша на стороне клиента
Для предсказуемых релизов придерживайтесь двух правил:
- Хеши в именах ассетов — длинный TTL и
immutable. - HTML и JSON‑манифесты — короткий TTL или без кэша, чтобы новые версии ассетов подтягивались мгновенно.
Отдельно проверьте, что в шаблонах HTML путь к ассетам действительно обновляется на новые имена. Большинство сборщиков генерируют манифест и автоматически подставляют пути.
Расширения: предпросмотр Pull Request и матрица окружений
Полезно добавить job, который собирает сайт для каждой ветки или PR и выгружает его в самостоятельный префикс каталога, например ~/previews/pr-123/. Это не заменяет продовый деплой, но сильно помогает в тестировании. Очистку таких окружений привязывайте к закрытию PR.

Итоги
Статический сайт на виртуальном хостинге плюс GitHub Actions и rsync — надёжная, быстрая и экономичная схема. Один раз настроили SSH‑ключи, подобрали флаги, оформили правила кэша — и дальше деплой занимает секунды. Для безболезненных релизов используйте --delay-updates или промежуточный каталог, храните несколько релизов для откатов и держите секреты в порядке. Остальное за вас сделает автоматизация.


