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

CI/CD для PHP на VDS: GitHub Actions, rsync/SSH и .env секреты

Практический конвейер CI/CD для PHP на VDS: настройка сервера, GitHub Actions, деплой rsync по SSH, безопасная работа с .env, релизы с симлинком current, быстрый откат, миграции, OPCache и защита пайплайна. Подходит для Laravel, Symfony, WordPress и самописных проектов.
CI/CD для PHP на VDS: GitHub Actions, rsync/SSH и .env секреты

Зачем PHP-проекту CI/CD на VDS

Когда проект живет на VDS, главная боль — стабильный и повторяемый деплой без правок «на проде». CI/CD снимает риски: сборка и проверки идут в чистой среде, на сервер попадает только то, что прошло пайплайн, а переключение версий делается атомарно. Для PHP это критично: кэш опкода, миграции БД, права на файлы, секреты в .env — всё должно быть под контролем и воспроизводимо.

В этой статье показываю рабочую схему: GitHub Actions собирает приложение, деплоит на VDS через rsync/SSH, на сервере формируются релизы и символическая ссылка current, секреты не утекут, а откат возможен одной командой.

Архитектура пайплайна

Базовая схема простая:

  • GitHub Actions — сборка, тесты, подготовка артефактов, установка SSH-ключа и деплой.
  • rsync по SSH — быстрая передача только изменившихся файлов на VDS.
  • Каталоги релизов на сервере: /var/www/app/releases/<timestamp> и симлинк /var/www/app/current, который указывает на активную версию.
  • Секреты в .env живут вне релизов (в shared) и подлинковываются в каждый релиз.

Смысл релизов в том, чтобы переключение на новую версию происходило мгновенно, без состояния «полусобранного» деплоя. А откат — это просто смена симлинка на предыдущую папку.

Структура каталогов релизов на сервере: releases, current и общий .env

Подготовка VDS

Системный пользователь и директории

Создайте отдельного пользователя для деплоя и структуру директорий (пример: пользователь deploy, корень приложения /var/www/app):

adduser --disabled-password --gecos "" deploy
mkdir -p /var/www/app/releases
mkdir -p /var/www/app/shared
chown -R deploy:www-data /var/www/app
chmod 2775 /var/www/app /var/www/app/releases /var/www/app/shared

Бит 2 (setgid) на директориях гарантирует, что новые файлы получают группу www-data. Это снижает проблемы прав при работе PHP-FPM.

Где хранить .env

Файл .env храните в /var/www/app/shared/.env с ограниченными правами:

touch /var/www/app/shared/.env
chown deploy:www-data /var/www/app/shared/.env
chmod 640 /var/www/app/shared/.env

В каждом релизе он должен быть подлинкован на этот файл: releases/<timestamp>/.env -> ../shared/.env. Так секреты не копируются по каждому релизу и остаются в одном месте.

Веб-сервер

Корень сайта указывайте на /var/www/app/current/public (если это Laravel/Symfony) или просто /var/www/app/current для классических PHP-проектов. После переключения симлинка перезапуск веб-сервера не требуется, но полезно перезагрузить PHP-FPM для сброса опкода.

SSH-ключи и безопасность

Сгенерируйте отдельную пару ключей под деплой, публичный ключ положите в ~deploy/.ssh/authorized_keys. В sshd_config включите вход только по ключам и ограничьте доступ по пользователю:

PermitRootLogin no
PasswordAuthentication no
AllowUsers deploy

Перезапустите SSH и проверьте вход. На стороне GitHub Actions приватный ключ сохраните как секрет репозитория.

FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды

Секреты и .env: два практичных паттерна

1) Сервер — «источник истины»

Простой и безопасный подход: .env создаётся и редактируется только на сервере. Пайплайн никогда не видит содержимое секретов. В момент деплоя мы лишь подлинковываем файл в новый релиз. Этот вариант минимизирует риски утечки через логи CI/CD.

2) .env собирается в пайплайне

Если нужно управлять конфигурацией централизованно, собирайте .env из секретов GitHub Actions и отправляйте его в shared. Важно: не печатать содержимое файла в логи и не коммитить его. Запрещаем историю, даём файлу минимальные права и сразу стираем локальную копию после отправки.

Пример workflow GitHub Actions

Сценарий ниже делает следующее: чекаут кода, подготовка PHP, установка зависимостей, формирование номера релиза, загрузка кода на VDS по rsync, установка зависимостей уже на сервере, прогрев кэшей, атомарное переключение current, очистка старых релизов.

name: ci-cd-php

on:
  push:
    branches: [ "main" ]
  workflow_dispatch: {}

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      SSH_HOST: ${{ secrets.SSH_HOST }}
      SSH_PORT: ${{ secrets.SSH_PORT }}
      SSH_USER: ${{ secrets.SSH_USER }}
      REMOTE_DIR: /var/www/app
      PHP_VERSION: '8.2'
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHP_VERSION }}
          extensions: mbstring, intl, zip, bcmath, pdo_mysql
          coverage: none

      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: ~/.composer/cache
          key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}

      - name: Install dependencies (dev)
        run: composer install --no-interaction --prefer-dist

      # Если нужно собирать фронтенд, добавьте шаги npm ci / build

      - name: Create release id
        run: |
          REL=$(date +%Y%m%d%H%M%S)
          echo "RELEASE=$REL" >> $GITHUB_ENV

      - name: Prepare SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_deploy
          chmod 600 ~/.ssh/id_deploy
          ssh-keyscan -p $SSH_PORT $SSH_HOST >> ~/.ssh/known_hosts

      # Если .env собирается в CI, раскомментируйте блок ниже
      # - name: Build .env from secrets
      #   run: |
      #     cat > .env << 'EOF'
      #     APP_ENV=production
      #     APP_DEBUG=false
      #     APP_KEY=${{ secrets.APP_KEY }}
      #     DB_HOST=${{ secrets.DB_HOST }}
      #     DB_DATABASE=${{ secrets.DB_NAME }}
      #     DB_USERNAME=${{ secrets.DB_USER }}
      #     DB_PASSWORD=${{ secrets.DB_PASS }}
      #     EOF

      - name: Rsync code to release
        run: rsync -az --delete --exclude ".git" --exclude ".github" --exclude "node_modules" --exclude "vendor" -e "ssh -i ~/.ssh/id_deploy -p $SSH_PORT" ./ $SSH_USER@$SSH_HOST:$REMOTE_DIR/releases/$RELEASE/

      # Если .env собирается в CI, отправим его на сервер и удалим локально
      # - name: Upload .env to shared
      #   run: |
      #     rsync -az -e "ssh -i ~/.ssh/id_deploy -p $SSH_PORT" .env $SSH_USER@$SSH_HOST:$REMOTE_DIR/shared/.env
      #     shred -u .env

      - name: Remote install and switch
        run: ssh -i ~/.ssh/id_deploy -p $SSH_PORT $SSH_USER@$SSH_HOST "cd $REMOTE_DIR/releases/$RELEASE && ln -sfn $REMOTE_DIR/shared/.env .env && composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader && php artisan config:cache || true && php artisan route:cache || true && php artisan view:cache || true && ln -sfn $REMOTE_DIR/releases/$RELEASE $REMOTE_DIR/current && chown -h deploy:www-data $REMOTE_DIR/current && sudo systemctl reload php8.2-fpm || true"

      - name: Cleanup old releases (keep 5)
        run: ssh -i ~/.ssh/id_deploy -p $SSH_PORT $SSH_USER@$SSH_HOST "cd $REMOTE_DIR/releases && ls -1t | tail -n +6 | xargs -r rm -rf"

Пояснения:

  • Зависимости Composer ставим на сервере внутри релиза, чтобы не тянуть vendor по сети. Убедитесь, что Composer установлен на VDS.
  • Опциональные команды artisan помечены || true, чтобы не обрушить деплой на проектах без Laravel. Подставьте свои команды сборки/кэшей (Symfony, WordPress MU-plugins и т.п.).
  • Переключение current делается после сборки, что обеспечивает атомарность.

Обзор workflow GitHub Actions для деплоя PHP через SSH и rsync

Релизы и откаты

Каждая версия создаётся в отдельной папке, затем обновляется симлинк current. Если релиз неудачный, откат — это выбор предыдущей папки и смена ссылки:

cd /var/www/app
PREV=$(ls -1t releases | sed -n '2p')
ln -sfn /var/www/app/releases/$PREV /var/www/app/current
sudo systemctl reload php8.2-fpm

Держите 5–10 релизов для отладки. При миграции проекта без простоя может пригодиться чек-лист из статьи про перенос сайтов: переезд без простоя.

Права, группы и владельцы

Типичные ошибки деплоя связаны с правами. Рекомендации:

  • deploy должен быть владельцем файлов, а группа — www-data (или группа PHP-FPM). Директории с setgid.
  • Каталоги для записываемых данных (например, storage у Laravel, wp-content/uploads у WordPress) вынесите в shared и подлинкуйте в релиз.
  • Не давайте 777; достаточно 750/640 при корректных владельцах.

Оптимизация rsync

rsync хорош тем, что передаёт только изменения и сохраняет права. Полезные флаги:

  • -a — архивный режим (права, время, симлинки);
  • -z — сжатие по сети;
  • --delete — чистит файлы, удалённые в репозитории (безопаснее применять в каталоге релиза, а не в current);
  • --info=progress2 — для диагностики при ручных прогонах.

Сброс OPCache и перезагрузка PHP-FPM

После переключения релиза полезно сбросить коды в OPCache. Самый простой способ — мягкая перезагрузка PHP-FPM. Для высокой нагрузки рассмотрите управляемый сброс через административный endpoint приложения (безопасно ограниченный) — но не держите публичных скриптов, сбрасывающих кэш, в веб-корне.

Миграции БД без простоя

Хорошая практика — выполнять миграции до переключения симлинка, если они обратно совместимы. Иначе используйте последовательность: включить режим обслуживания, применить миграции, прогреть кэши, переключить current, выключить режим обслуживания. Для Laravel это php artisan down/up и migrate --force.

Управление несколькими окружениями

Для staging и production создайте два деплоя: раздельные секреты, разные серверы/порты/пользователи, отдельные .env. В GitHub Actions удобно назначать environments с обязательным подтверждением релиза на проде. Теги (vX.Y.Z) можно использовать как триггер production-пайплайна.

Безопасность пайплайна

  • Храните приватный ключ деплоя только в секретах CI. Публичный — узкоспециализированный, отдельный от личных ключей.
  • Ограничьте пользователя deploy минимумом прав. Не выдавайте sudo без необходимости.
  • Проверяйте ключ хоста: ssh-keyscan добавляет отпечаток в known_hosts; фиксируйте его и не перезаписывайте без проверки.
  • Фильтруйте логи: не печатайте секреты, не включайте избыточный verbose там, где это может раскрыть конфиденциальные строки.

Диагностика и частые проблемы

  • Permission denied при записи — проверьте владельцев и группу, а также setgid на директориях.
  • No such file or directory .env — забудьте создать shared/.env или симлинк в релизе.
  • Class not found после деплоя — выполните composer dump-autoload -o внутри релиза и сбросьте OPCache.
  • 502/504 сразу после релиза — проверьте логи PHP-FPM и веб-сервера; возможно, кэш маршрутов несовместим с кодом или миграции не применены.
  • rsync: mkstemp failed — нет прав на каталог назначения или мало места на диске.

Вариант: деплой только артефактов

Если сборка тяжёлая (например, фронтенд), логичнее собирать артефакты в CI (включая vendor) и выкатывать их единым архивом. Тогда на сервере не нужен Composer/Node.js. Минус — больший трафик. Плюс — детерминированная сборка в чистой среде. Для фоновых очередей и воркеров посмотрите, как поднимать workers через systemd: очереди и Supervisor.

Резюме

CI/CD на VDS для PHP-проектов можно держать максимально прозрачным: GitHub Actions + rsync/SSH, релизы в отдельных директориях, симлинк current, аккуратная работа с .env и простой откат. Начните с минимальной схемы, добавьте миграции, кэши и проверки, и только потом усложняйте. Такая архитектура масштабируется от небольших сайтов до крупных приложений и не привязана к конкретному фреймворку.

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

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

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...