Новинка Виртуальный VDS сервер в Нидерландах от 390р
Выберите продукт

CI/CD деплой через rsync: артефакты и быстрые rollbacks

Разбираем практическую схему CI/CD с деплоем по rsync: от сборки артефактов в CI до выката на сервер. Обсудим, какие файлы включать в релизный пакет, как обновлять сайт без простоя, хранить несколько версий и делать быстрый rollback на GitHub Actions и GitLab CI.
CI/CD деплой через rsync: артефакты и быстрые rollbacks

Деплой по rsync через CI до сих пор остаётся одним из самых простых и надёжных способов выката веб‑проектов. Не нужен Docker, нет привязки к конкретному оркестратору, можно работать и с простым виртуальным хостингом, и с собственным VDS. При этом у такого подхода есть два частых «камня преткновения»:

— что именно считать артефактами сборки и как их передавать между шагами в CI;
— как организовать безопасный деплой с возможностью быстрого rollback без ручного копания на сервере.

В этой статье разберём практическую схему CI/CD (на примерах GitHub Actions и GitLab CI), в которой:

  • сборка выполняется в CI‑окружении, а на сервер уезжают только артефакты;
  • выкат на прод происходит по rsync поверх SSH;
  • на сервере хранится несколько версий релизов и реализован быстрый откат;
  • всё это можно адаптировать под PHP/Node.js/статические сайты.

Зачем вообще артефакты в CI/CD и почему rsync

Грубая схема классического деплоя по rsync выглядит так: CI‑runner просто «заливает» на сервер ваш репозиторий, а на сервере уже крутятся composer install, npm install, npm run build и прочее.

Проблема: сервер превращается в помойку зависимостей и билд‑скриптов, билды могут отличаться от окружения к окружению, сильно растёт время выката. К тому же деплой становится «толстым» — можно легко схлопотать простой, если сборка на проде затянется или что‑то пойдёт не так.

Подход с артефактами предлагает другое:

  • вся сборка происходит в CI‑окружении (GitHub Actions / GitLab CI / другой runner);
  • на выходе мы получаем готовый «пакет релиза» — каталог release/ с build/, нужными vendor/, node_modules/ и конфигами без секретов;
  • в деплое по rsync мы передаём только этот артефакт, а не весь репозиторий;
  • на сервере мы не билдим ничего, только переключаем симлинк current и (опционально) запускаем миграции.

Результат:

  • быстрый и предсказуемый деплой;
  • чистое прод‑окружение без dev‑зависимостей;
  • возможность хранить несколько релизов и откатываться к любому из них за секунды.

Что считать артефактами: типовые наборы файлов

Набор артефактов зависит от стека, но есть типовые паттерны. Главное правило: в деплойный артефакт не попадают секреты и всё, что может или должно быть специфичным для окружения (prod/stage/dev) и сервера (лог‑директории, кеш и т.п.).

PHP‑приложения (Laravel / Symfony / CMS)

Обычно в артефакт идут:

  • собранные фронтенд‑ассеты: public/build/ или public/dist/;
  • директория с продакшен‑зависимостями: vendor/ (после composer install --no-dev);
  • исходники приложения: app/, src/, шаблоны, маршруты, переводы;
  • базовые конфиги без секретов: шаблоны .env.example, файлы config/*.php с привязкой к переменным окружения;
  • migrations и seeders (если используются).

Не включаем:

  • реальный .env (секреты должны задаваться переменными окружения или лежать в недоступном из сети файловом хранилище);
  • папки storage/logs, storage/framework/cache, storage/sessions — они живут на сервере и не переезжают между релизами;
  • dev‑зависимости (require-dev).

Node.js / SPA / статические сайты

Для SPA и статических сайтов часто достаточно одного каталога dist/ либо build/, который создаётся командой npm run build или аналогичной.

Если это Node.js‑SSR или backend‑приложение, обычно требуется:

  • собранный JS‑код (если используется TypeScript/Webpack/Vite);
  • node_modules/ c продакшен‑зависимостями (npm ci --omit=dev или pnpm install --prod);
  • конфиги без секретов, завязанные на process.env.*;
  • скрипты миграций БД (если есть).

Как структурировать артефакты для удобного rsync

Удобно, когда в CI вы собираете всё в единый каталог, например release/:

release/
  app/
  vendor/
  public/
  config/
  database/

Или для фронтенда:

release/
  dist/
  server.js
  node_modules/

Тогда в job деплоя у вас будет один простой rsync из локального release/ в удалённый releases/<hash>/.

Схема директорий releases, current и shared для деплоя и откатов релизов

Паттерн директорий на сервере: releases + current + shared

Чтобы иметь возможность быстрых откатов и не портить прод‑окружение, удобно использовать классический паттерн директорий:

  • /var/www/myapp/releases/ — каталог с подкаталогами по релизам, например по SHA коммита или дате;
  • /var/www/myapp/shared/ — общие файлы и каталоги между релизами: логи, кеш, .env, загруженные пользователями файлы;
  • /var/www/myapp/current — симлинк на текущий активный релиз.

Идея такая: CI и rsync выкладывают новую сборку в releases/<id>, потом деплой‑скрипт на сервере обновляет симлинк current на новый релиз (и при необходимости запускает миграции, рестарт PHP‑FPM, PM2 и т.д.). Откат — это простое переключение симлинка на предыдущий релиз.

Простейший скелет директорий:

/var/www/myapp
  releases/
    2025-02-10_120501_4bdc1a9/
    2025-02-09_231045_c8e2741/
  shared/
    .env
    storage/
      logs/
      cache/
      sessions/
    public/
      uploads/
  current -> /var/www/myapp/releases/2025-02-10_120501_4bdc1a9

При каждом деплое:

  1. CI собирает release/ и шлёт его по rsync в новый каталог в releases/;
  2. серверный скрипт создаёт или обновляет нужные симлинки от релиза на каталоги в shared/ (например, storage и public/uploads);
  3. выполняются миграции БД (если нужно);
  4. симлинк current атомарно переключается на новый релиз;
  5. опционально рестартуются сервисы (php‑fpm не обязательно, чаще нужен только reload nginx или рестарт node‑процесса).

Такой шаблон отлично масштабируется от типовых PHP‑сайтов до проектов с несколькими окружениями на отдельных серверах или облачных VDS.

Базовый rsync‑деплой: ключевые параметры

Сильно упрощённый вариант команды деплоя:

rsync -az --delete --exclude ".git" --exclude ".github" release/ user@prod:/var/www/myapp/releases/2025-02-10_120501_4bdc1a9/

Разберём ключи:

  • -a — архивный режим (права, рекурсия и пр.);
  • -z — сжатие по сети, полезно при медленном канале;
  • --delete — удалять файлы на сервере, которых больше нет в источнике; позволяет поддерживать чистоту каталога релиза;
  • --exclude — исключения, чтобы не протащить лишнее (CI‑конфиги, тесты, git‑метаданные).

Для production‑деплоя стоит добавить:

  • --partial и --inplace по ситуации, если у вас большие файлы и нужно экономить трафик;
  • --chown=www-data:www-data или --chmod, если нужно задавать владельцев и права прямо при копировании;
  • --rsync-path, если rsync на сервере лежит не по умолчанию или нужен sudo rsync.

Важно: деплой по rsync запускаем против releases/<id>, а не против current. Симлинк меняет уже отдельный скрипт.

Оркестрация деплоя на сервере и схема rollback

Чтобы CI не знал деталей внутренней кухни сервера, удобно иметь один «входной» скрипт, например /usr/local/bin/deploy-myapp.sh, который принимает ID релиза и делает всё остальное.

Скелет такого скрипта:

#!/bin/bash
set -euo pipefail

APP_DIR="/var/www/myapp"
RELEASES_DIR="$APP_DIR/releases"
SHARED_DIR="$APP_DIR/shared"
CURRENT_LINK="$APP_DIR/current"

NEW_RELEASE_ID="$1"  # например 2025-02-10_120501_4bdc1a9
NEW_RELEASE_DIR="$RELEASES_DIR/$NEW_RELEASE_ID"

if [ ! -d "$NEW_RELEASE_DIR" ]; then
  echo "Release $NEW_RELEASE_DIR does not exist" 1>&2
  exit 1
fi

ln -sfn "$SHARED_DIR/.env" "$NEW_RELEASE_DIR/.env"
ln -sfn "$SHARED_DIR/storage" "$NEW_RELEASE_DIR/storage"
ln -sfn "$SHARED_DIR/public/uploads" "$NEW_RELEASE_DIR/public/uploads"

cd "$NEW_RELEASE_DIR"
php artisan migrate --force

ln -sfn "$NEW_RELEASE_DIR" "$CURRENT_LINK"

systemctl reload php8.2-fpm

Rollback в этой схеме — это или отдельный скрипт, который находит предыдущий релиз и переключает current на него, или просто ручная команда:

cd /var/www/myapp
ln -sfn releases/2025-02-09_231045_c8e2741 current
systemctl reload php8.2-fpm

Чтобы автоматизировать откаты, можно вести простой releases.log или хранить список релизов и их статусов в отдельном файле или БД, но для начала часто достаточно «последнего успешного» релиза из каталога.

Схематичный пример пайплайнов GitHub Actions и GitLab CI с деплоем через rsync

Связка rsync + GitHub Actions: пример workflow

Теперь соберём всё воедино в контексте GitHub Actions. Сценарий:

  1. на пуш в main запускается workflow;
  2. собираются артефакты (PHP+JS);
  3. артефакт пакуется в release/;
  4. по SSH и rsync релиз отправляется на сервер и вызывается deploy-myapp.sh;
  5. при ошибке миграций можно запустить откат вручную отдельным job или на сервере.

Упрощённый пример .github/workflows/deploy.yml (без учёта секретов и всех оптимизаций):

name: Deploy via rsync

on:
  push:
    branches: ["main"]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.2"

      - name: Install composer deps
        run: composer install --no-dev --prefer-dist --no-interaction

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install npm deps and build
        run: npm ci && npm run build

      - name: Prepare release directory
        run: mkdir -p release && rsync -a --exclude ".git" --exclude ".github" --exclude "node_modules" ./ ./release/ && rm -rf release/tests release/.github release/.git

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: release
          path: release

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: release
          path: release

      - name: Setup SSH
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: mkdir -p ~/.ssh && echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa && echo "${{ secrets.KNOWN_HOSTS }}" > ~/.ssh/known_hosts

      - name: Rsync to server
        run: RELEASE_ID=$(date +"%Y-%m-%d_%H%M%S")_${{ github.sha }} && rsync -az --delete release/ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/myapp/releases/$RELEASE_ID/ && ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "/usr/local/bin/deploy-myapp.sh $RELEASE_ID"

Ключевые моменты:

  • артефакт передаётся между job через upload-artifact/download-artifact;
  • RELEASE_ID строится из даты и SHA — легко отыскать нужный релиз на сервере;
  • секреты (SSH_PRIVATE_KEY, хост, юзер) хранятся в GitHub Secrets, а не в репозитории;
  • логика миграций и симлинков инкапсулирована в скрипте на сервере.

GitLab CI: аналогичный пайплайн с артефактами и rsync

В GitLab CI концепция такая же, только слова другие: job, стейджи, artifacts и т.д. Схема:

  • стейдж build — собирает приложение, складывает всё в release/ и объявляет это артефактом;
  • стейдж deploy — получает артефакт, делает rsync на сервер и запускает скрипт деплоя.

Пример .gitlab-ci.yml:

stages:
  - build
  - deploy

variables:
  APP_DIR: "/var/www/myapp"

build:
  stage: build
  image: php:8.2-cli
  script:
    - apt-get update
    - apt-get install -y git unzip nodejs npm rsync
    - composer install --no-dev --prefer-dist --no-interaction
    - npm ci
    - npm run build
    - mkdir -p release
    - rsync -a --exclude ".git" --exclude ".gitlab" --exclude "node_modules" ./ ./release/
  artifacts:
    paths:
      - release/
    expire_in: 1 week

production-deploy:
  stage: deploy
  image: debian:stable-slim
  needs:
    - build
  script:
    - apt-get update
    - apt-get install -y rsync openssh-client
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts
    - RELEASE_ID=$(date +"%Y-%m-%d_%H%M%S")_${CI_COMMIT_SHORT_SHA}
    - rsync -az --delete release/ "$SSH_USER@$SSH_HOST:$APP_DIR/releases/$RELEASE_ID/"
    - ssh "$SSH_USER@$SSH_HOST" "$APP_DIR/deploy-myapp.sh $RELEASE_ID"
  environment:
    name: production
    url: https://example.com
  only:
    - main

Несколько нюансов:

  • для artifacts лучше задать разумное expire_in, чтобы не забивать хранилище GitLab;
  • секреты задаются в Settings → CI/CD → Variables и попадают в job через переменные среды;
  • логика rollback будет той же: отдельный скрипт на сервере, который можно вызвать вручную или через отдельный manual-job.

Rollback: стратегии и подводные камни

Откат релиза — это не только переключение файлов. Есть подводные камни, о которых важно помнить.

Миграции БД и несовместимые изменения

Самый распространённый сценарий проблемного отката:

Новый релиз выполнил миграцию, которая меняет структуру таблицы или удаляет столбец, а старый код с этим не дружит. После rollback сайт перестаёт работать, хоть мы и откатили файлы.

Что можно сделать:

  • использовать стратегию миграций «forwards‑compatible schemas»: сначала деплой, который добавляет новые поля или таблицы, но не ломает старый код; позже — деплой, который начинает эти поля использовать и только потом удаляет старое;
  • иметь миграции «вперёд» и «назад» (up/down), с возможностью откатить схему вместе с кодом (подходит не всегда, особенно на больших базах);
  • минимизировать разрушающие изменения в горячих таблицах; сложные миграции чаще выполнять отдельно от регулярного CI‑деплоя.

Хранение нескольких релизов и сборка индекса

Чтобы rollback был возможен, храните несколько последних релизов в releases/. Типичная практика — 5–10 релизов, остальное периодически убирает cron:

#!/bin/bash
set -euo pipefail

APP_DIR="/var/www/myapp"
cd "$APP_DIR/releases"

ls -1t | tail -n +11 | xargs -r rm -rf

Разумно также вести индекс релизов: кто выкатил, из какого коммита, какой статус (успех или провал). Это можно делать как минимум логированием в deploy.log, а лучше — писать запись из CI (через SSH) в отдельный файл или даже в маленькую служебную БД.

Проверка здоровья после релиза

Чтобы понять, что после выкатки всё живо, удобно встроить шаг health‑check:

  • CI после деплоя делает HTTP‑запрос на /health или /status и проверяет код ответа;
  • при неудаче или тайм‑ауте можно автоматически инициировать rollback (сложнее, требует аккуратной логики, чтобы не устроить «качели» между релизами);
  • для начала часто достаточно хотя бы ручной проверки и понятной инструкции по откату.

Для проектов на современных CMS и фреймворках дополнительные оптимизации можно делать отдельно: кеш, очереди, Redis, тонкая настройка PHP. Об этом подробно написано в материале про использование Redis для PHP‑сессий и object‑cache.

Практические советы по безопасности и стабильности

Несколько вещей, которые часто забывают при построении CI/CD‑цепочки с SSH и rsync.

SSH‑ключи и права

Лучший вариант — отдельный системный пользователь на сервере, которому разрешён доступ только к нужной директории и скриптам деплоя. Ограничьте его authorized_keys командой и опциями:

command="/usr/local/bin/deploy-shell-wrapper.sh",no-agent-forwarding,no-port-forwarding,no-pty ssh-ed25519 AAAA... ci-deploy-key

Так CI‑ключ не сможет произвольно выполнять команды.

Идемпотентность скриптов деплоя

Скрипт на сервере должен выдерживать повторный запуск с тем же RELEASE_ID без разрушения состояния:

  • создание симлинков — только через ln -sfn;
  • миграции — должны уметь пропускать уже применённые шаги;
  • никаких rm -rf current до обновления, только обновление ссылки.

Логирование деплой‑скриптов

Все ключевые шаги (создание релиза, выполнение миграций, переключение симлинка, рестарт сервисов) логируйте в отдельный файл с отметкой времени. Это сильно помогает при расследовании причин падения и при обучении команды пользоваться rollback.

Для проектов, которые растут по нагрузке, особенно на PHP и Node.js, полезно заранее закладываться на масштабирование и производительность. Для этого подойдёт перенос на быстрый облачный VDS с достаточным запасом по ресурсам и гибкой настройкой окружения.

Итоги

Построить надёжный CI/CD с использованием rsync, артефактов и возможностью быстрых откатов вполне реально без тяжёлой инфраструктуры. Ключевые элементы такой схемы:

  • сборка артефактов в CI, а не на продакшене;
  • структура releases/current/shared на сервере;
  • деплой только в новый каталог релиза и атомарное переключение симлинка;
  • миграции БД с учётом обратной совместимости и стратегии rollback;
  • минимальные, но достаточные проверки здоровья приложения после выката.

Такой подход хорошо масштабируется: от простого PHP‑сайта на обычном хостинге до крупных проектов на отдельных серверах или VDS с несколькими окружениями и сложными пайплайнами GitHub Actions и GitLab CI. Главное — один раз настроить понятный процесс и не бояться автоматизировать рутину, чтобы откат релиза перестал быть «ручной магией по SSH ночью», а превратился в быструю и предсказуемую операцию.

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

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

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...