Git давно стал стандартом для разработки, но на продакшн‑серверы код до сих пор часто выкладывают через SFTP или rsync "вручную". Это больно, медленно и приводит к ошибкам: забытые файлы, несинхронизированные ветки, случайный rm -rf в корне сайта.
В этой статье разберём практичную схему:
- репозиторий проекта живёт на GitHub или GitLab;
- на VDS есть свой bare‑репозиторий (git‑зеркало);
- деплой на боевой каталог происходит через git‑hook или отдельным скриптом;
- доступ – только по SSH‑ключам, минимум прав, никаких паролей.
Это не заменяет полноценный CI/CD, но даёт простой и предсказуемый деплой, который реально удобно использовать на одиночном VDS‑сервере.
Подходы к деплою через git на VDS
Сначала определимся с архитектурой. Есть три основных паттерна деплоя кода на VDS через git:
- push из локальной машины в VDS – вы пушите из своего ноутбука прямо в репозиторий на сервере;
- pull с VDS из GitHub/GitLab – сервер сам делает
git pullиз удалённого репо; - комбинированная схема (зеркало + hook) – VDS держит bare‑репозиторий, обновляемый с GitHub/GitLab, и из него раскатывает код в рабочую директорию.
Рассмотрим плюсы и минусы самых практичных вариантов и соберём из них рабочую схему.
Вариант 1: git push напрямую на VDS
Классическая схема: на VDS создаётся bare‑репозиторий, вы добавляете его как remote в локальный git и делаете git push vds main. В post-receive‑хуке этот bare‑репозиторий раскатывает рабочую копию в каталог сайта.
Плюс – отсутствие зависимости от сторонних сервисов. Минус – вам нужно открывать SSH наружу к VDS и раздавать доступ всем разработчикам прямо на боевой сервер.
Такой вариант часто используют в маленьких командах или для внутренних сервисов без GitHub/GitLab. В статье мы его коснёмся, но основной фокус будет на связке с внешним репозиторием.
Вариант 2: VDS сам делает git pull с GitHub/GitLab
Тут рабочая копия сайта на сервере сама привязана к удалённому репозиторию:
- один раз делаем
git cloneс GitHub/GitLab на VDS; - при деплое заходим на сервер и выполняем
git pull origin main; - опционально – оборачиваем это в скрипт или обвязку в CI.
Плюсы:
- репозиторий остаётся централизованным (GitHub/GitLab);
- удобная интеграция с CI: раннер может сам ходить на VDS и запускать деплой‑скрипт.
Минусы:
- нужно хранить деплой‑ключи на VDS, следить за правами и ротацией;
- надо аккуратно избегать ручных коммитов прямо в рабочую копию на сервере (иначе конфликты при
pull).
Вариант 3: bare‑зеркало на VDS + деплой из него
Более гибкий вариант, которым и будем пользоваться:
- на VDS создаётся bare‑репозиторий (без рабочей копии);
- GitHub/GitLab пушит в него через deploy key или CI;
- hook bare‑репозитория обновляет один или несколько рабочих каталогов (prod, staging и т.п.).
Плюсы:
- можно легко делать несколько сред из одного репозитория;
- hook централизованно контролирует, как и куда обновлять код;
- можно повесить доп. логику (composer, npm, миграции, рестарт сервисов).
Минусы:
- настройка сложнее, чем просто
git pullв рабочем каталоге; - нужно внимательно продумать права на каталоги и пользователя деплоя.
Ниже соберём рабочую схему, которая подойдёт и для GitHub, и для GitLab, и не будет зависеть от конкретного фреймворка.
Подготовка пользователя и каталогов на VDS
Дальше все примеры будут даны для Linux‑VDS (Debian/Ubuntu‑семейство), доступ по SSH от имени root или пользователя с sudo.
Если вы только переносите проект с обычного виртуального хостинга на отдельный сервер, удобнее сразу продумать структуру каталогов и пользователей, чтобы потом не переезжать второй раз.
Создаём отдельного пользователя для деплоя
Не стоит деплоить от root. Создадим пользователя, скажем, deploy:
adduser --disabled-password --gecos "Deploy user" deploy
Положим каталоги так:
/home/deploy/repos/myproject.git– bare‑репозиторий;/var/www/myproject– рабочая копия сайта (отдаётся веб‑сервером);/var/www/myproject_releases– опционально, если будете делать релизы по релизным каталогам.
Создадим базовую структуру:
mkdir -p /home/deploy/repos
mkdir -p /var/www/myproject
chown -R deploy:deploy /home/deploy
chown -R deploy:deploy /var/www/myproject
Настраиваем SSH‑доступ по ключу
Если у вас ещё нет SSH‑ключа на рабочей машине, сгенерируйте его:
ssh-keygen -t ed25519 -C "dev-on-laptop"
Скопируйте публичный ключ на VDS для пользователя deploy:
ssh-copy-id deploy@your-vds-host
Проверьте вход:
ssh deploy@your-vds-host
Пароль спрашиваться не должен, только подтверждение fingerprint при первом подключении.
Создание bare‑репозитория на VDS
Под пользователем deploy создадим bare‑репо:
ssh deploy@your-vds-host
cd ~/repos
mkdir myproject.git
cd myproject.git
git init --bare
Bare‑репозиторий нужен, чтобы принимать пуши, но не содержать рабочую копию файлов – это просто хранилище ссылок и объектов git.
Внутри bare‑репозитория будет каталог hooks. Мы будем использовать post-receive‑hook, который после успешного пуша развернёт код в /var/www/myproject.
Связка локального git с bare‑репозиторием на VDS
Теперь привяжем ваш локальный проект к репозиторию на VDS. В каталоге проекта:
git remote add vds deploy@your-vds-host:/home/deploy/repos/myproject.git
Проверьте список remotes:
git remote -v
Сделайте первый пуш (например, ветки main):
git push vds main
На этом этапе код ещё не появляется в каталоге сайта: он только попал в bare‑репозиторий. Теперь научим VDS выполнять деплой после каждого пуша.

Настройка git hook для деплоя на VDS
Hook post-receive в bare‑репозитории запускается после того, как в него пришли новые коммиты. Это удобная точка для деплоя.
Простой деплой без релизов
Для начала сделаем минимальный, но рабочий hook, который:
- обновит (или создаст) рабочую копию в
/var/www/myproject; - переключит её на нужную ветку (например,
main); - сделает
git reset --hardна последний коммит; - опционально выполнит команды сборки (composer, npm и т.п.).
Под пользователем deploy создадим или отредактируем файл /home/deploy/repos/myproject.git/hooks/post-receive:
#!/bin/bash
set -e
REPO_DIR="/home/deploy/repos/myproject.git"
WORK_TREE="/var/www/myproject"
BRANCH="main"
if [ ! -d "$WORK_TREE/.git" ]; then
mkdir -p "$WORK_TREE"
git --work-tree="$WORK_TREE" --git-dir="$REPO_DIR" checkout -f "$BRANCH"
else
git --work-tree="$WORK_TREE" --git-dir="$REPO_DIR" fetch origin "$BRANCH"
git --work-tree="$WORK_TREE" --git-dir="$REPO_DIR" checkout -f "$BRANCH"
fi
cd "$WORK_TREE"
# Примеры: раскомментируйте, если нужно
# composer install --no-dev --optimize-autoloader
# npm install
# npm run build
exit 0
Сделаем его исполняемым:
chmod +x /home/deploy/repos/myproject.git/hooks/post-receive
Несколько важных замечаний по этому скрипту:
set -eзаставит скрипт падать при ошибке любой команды – это хорошо, чтобы не получить полусобранный деплой;- используем
--work-treeи--git-dir, чтобы не инициализировать полноценный репозиторий в/var/www/myproject– там можно оставить только рабочие файлы; - в примерах сборки обязательно учитывайте, под кем выполняется hook (пользователь
deploy, его$PATH, версия PHP/Node и т.д.).
Теперь при каждом git push vds main код автоматически обновляется на сервере.
Интеграция с GitHub: деплой через push или CI
Если ваш "центральный" репозиторий живёт на GitHub, bare‑репо на VDS можно использовать как:
- дополнительный
remote, в который пушат разработчики; - "target" для GitHub Actions или другого CI, который пушит только из проверенного пайплайна.
Пуш из локального git и GitHub одновременно
Простейший вариант – оставить GitHub как основной origin, а VDS добавить вторым remote:
git remote get-url origin
# origin указывает на GitHub
git remote add vds deploy@your-vds-host:/home/deploy/repos/myproject.git
Теперь вы можете пушить так:
git push origin main
git push vds main
Чтобы не забывать про пуш на VDS, можно настроить alias в git или локальный скрипт, но важнее – договориться в команде, что деплой всегда делает ответственный человек (или CI).
Деплой через GitHub Actions
Более чистый вариант – деплой делает только CI после прохождения тестов. Схема такая:
- на VDS создаём SSH‑ключ, которым GitHub Actions будет логиниться на сервер;
- публичный ключ добавляем в
~/.ssh/authorized_keysпользователяdeploy; - приватный ключ кладём в Secrets репозитория на GitHub;
- в workflow GitHub Actions выполняем
git pushв bare‑репо на VDS.
Ключи генерируем на VDS под пользователем deploy:
ssh deploy@your-vds-host
cd ~
ssh-keygen -t ed25519 -f ~/.ssh/github-actions -C "github-actions-deploy"
Публичный ключ ~/.ssh/github-actions.pub добавьте в ~/.ssh/authorized_keys (если ssh-keygen сам не предложил).
Приватный ключ (содержимое файла github-actions) скопируйте и занесите как секрет, например, DEPLOY_SSH_KEY в настройках репозитория GitHub.
В .github/workflows/deploy.yml можно сделать джоб, который после сборки и тестов пушит в VDS:
name: Deploy to VDS
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up SSH
run: |
mkdir -p ~/.ssh
echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "Host vds-host" >> ~/.ssh/config
echo " HostName your-vds-host" >> ~/.ssh/config
echo " User deploy" >> ~/.ssh/config
echo " IdentityFile ~/.ssh/id_ed25519" >> ~/.ssh/config
- name: Add VDS remote
run: |
git remote add vds deploy@your-vds-host:/home/deploy/repos/myproject.git
git push vds main
Здесь важно:
- не показывать приватный ключ в логах (используем секреты GitHub);
- по возможности ограничить ключ в
authorized_keysтолько нужными хостами/командами через опцииfrom=,command=и т.п.; - настроить firewall так, чтобы SSH‑порт был доступен только из нужных подсетей, если это возможно.
Интеграция с GitLab: репозиторий и Runner → VDS
В GitLab схема похожая, но можно пойти двумя путями:
- использовать стандартный GitLab CI/CD, который пушит в bare‑репо на VDS;
- использовать GitLab Runner, установленный прямо на VDS, и деплоить локально.
GitLab CI/CD пушит в bare‑репо на VDS
Алгоритм тот же, что и с GitHub Actions:
- создаём SSH‑ключ для GitLab CI (на VDS или локально);
- публичный ключ помещаем в
authorized_keysпользователяdeploy; - приватный ключ добавляем в переменную GitLab CI (в Settings → CI/CD → Variables);
- в
.gitlab-ci.ymlописываем джоб, который пушит вdeploy@your-vds-host:/home/deploy/repos/myproject.git.
Пример простого пайплайна:
stages:
- test
- deploy
test:
stage: test
script:
- echo "Run your tests here"
deploy:
stage: deploy
only:
- main
before_script:
- mkdir -p ~/.ssh
- echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- ssh-keyscan your-vds-host >> ~/.ssh/known_hosts
script:
- git remote add vds deploy@your-vds-host:/home/deploy/repos/myproject.git
- git push vds main
Переменная DEPLOY_SSH_KEY должна содержать приватный ключ, который имеет доступ к пользователю deploy на VDS.
GitLab Runner на VDS
Если у вас есть root‑доступ к VDS, можно поставить GitLab Runner прямо на него. Тогда деплой превращается просто в локальные команды (без отдельного SSH): Runner сам выполняет скрипты на сервере.
Плюсы:
- нет SSH из CI в прод: Runner уже живёт на этом сервере;
- проще работать с локальными путями, сокетами, службами;
- можно использовать shell‑executor и не морочиться с Docker.
Минусы:
- необходимо следить за безопасностью Runner (ограничения проектов, доступы);
- при компрометации проекта с правом деплоя потенциально уязвим сам VDS.
Деплой‑задача в .gitlab-ci.yml в таком случае может просто выполнять:
script:
- cd /var/www/myproject
- git fetch origin main
- git checkout -f main
- composer install --no-dev --optimize-autoloader
Здесь origin – это GitLab. Вы один раз клонируете репозиторий в /var/www/myproject, а CI только подтягивает изменения.
Продвинутый деплой: релизные каталоги и symlink
Прямой git checkout -f в каталоге, который сейчас обслуживает продакшн, имеет очевидный минус: во время деплоя (особенно с длинным composer install или сборкой фронтенда) пользователи могут ловить ошибки.
Более надёжная схема – релизные каталоги, по сути упрощённый аналог Capistrano/Deployer:
/var/www/myproject/releases/<timestamp>– полный код очередного релиза;/var/www/myproject/current– symlink на активный релиз;/var/www/myproject/shared– общие ресурсы (логи, аплоады,.env).
Тогда деплой превращается в:
- собрать новый релиз в каталоге
releases/...; - настроить права, прогнать миграции, прогреть кэш;
- переключить
currentна новый релиз атомарной операциейln -sfn; - опционально удалить старые релизы.
Hook post-receive можно доработать под такую схему. Простейший набросок (без shared‑директорий и миграций):
#!/bin/bash
set -e
REPO_DIR="/home/deploy/repos/myproject.git"
APP_DIR="/var/www/myproject"
RELEASES_DIR="$APP_DIR/releases"
BRANCH="main"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
NEW_RELEASE="$RELEASES_DIR/$TIMESTAMP"
mkdir -p "$RELEASES_DIR"
git --work-tree="$NEW_RELEASE" --git-dir="$REPO_DIR" checkout -f "$BRANCH"
cd "$NEW_RELEASE"
# composer install --no-dev --optimize-autoloader
# npm install
# npm run build
ln -sfn "$NEW_RELEASE" "$APP_DIR/current"
# Удаляем старые релизы, оставляя, например, 5 последних
cd "$RELEASES_DIR"
ls -1t | tail -n +6 | xargs -r rm -rf
exit 0
Веб‑сервер (например, nginx) в таком случае должен быть настроен на root /var/www/myproject/current/public;. Пример настройки nginx со статическим кэшем и оптимизациями есть в статье про оптимизацию PHP, OPCache и Brotli – многие идеи оттуда применимы и на VDS.

Безопасность: SSH, права и защита от случайностей
При деплое через git на VDS важно не только "чтобы работало", но и чтобы не превратить сервер в проходной двор.
Ограничиваем доступ по SSH
Базовые рекомендации:
- запретите вход по паролю (только ключи) – настройка
PasswordAuthentication noв/etc/ssh/sshd_config; - по возможности ограничьте вход только нужным пользователям (
AllowUsers deploy rootи т.п.); - используйте нестандартный порт SSH только как дополнительный шум, но не как основную защиту.
После изменения sshd_config не забывайте перезапустить SSH‑демон и не закрывать активную сессию, пока не убедились, что вход по новым настройкам работает.
Права на файлы и каталоги
Типичные роли:
- веб‑сервер (nginx/Apache) работает от пользователя
www-data; - деплой выполняется от пользователя
deploy; - часть каталогов (аплоады, логи) должна быть доступна обеим ролям.
Базовая схема:
- группа
www-dataдобавлена к пользователюdeployили наоборот; umaskдля деплоя настроен так, чтобы файлы были доступны группе на чтение;- права на каталоги – минимум
755, на файлы –644(если нужно только чтение).
Следите, чтобы в репозиторий не попадали конфиги с секретами (.env, ключи и т.п.). Лучше держать их в /var/www/myproject/shared или отдельном каталоге с ограниченными правами, а в коде ссылаться на них через переменные окружения и симлинки. Для хранения и выката секретов в git‑ориентированных пайплайнах можно использовать подходы из статьи про SOPS и GitOps‑секреты.
Типичные грабли и как их обходить
Ручные правки на сервере
Самая частая проблема: кто-то правит файлы прямо в /var/www/myproject, а затем git push затирает эти изменения.
Никаких правок кода на сервере. Всё только через git.
Если нужны быстрые эксперименты – отдельная ветка и отдельная среда (staging), но не прод.
Конфликты при git pull
Если вы используете схему без bare‑репо и делаете git pull прямо в рабочей копии, не делайте там git commit. Иначе при pull легко поймать конфликты, которые придётся решать вручную (на проде!).
Лучше или перейти на bare‑схему с hook, или жёстко запретить коммиты в рабочей директории.
Зависимости и бинарники в репозитории
Иногда складывают в репозиторий всё подряд: vendor, node_modules, собранные JS/CSS, картинки в нескольких копиях. Это раздувает репозиторий и делает пуши медленнее.
Рекомендации:
- в идеале хранить в git только исходники, а сборку и установку зависимостей делать в hook или CI;
- если сборка тяжёлая и занимает минуты, собирать артефакт в CI и выкладывать его на VDS по SSH или rsync, а git использовать как триггер деплоя;
- обязательно держать
.gitignoreв порядке, не коммитить временные файлы и кэш.
Сломанный деплой из-за окружения
Разные версии PHP/Node, отсутствие composer/npm на сервере, отличия в модулях – всё это может выстрелить только на проде, если собирать проект там же.
Минимизировать риски можно так:
- собирать проект в CI в максимально похожем окружении на VDS;
- фиксировать версии зависимостей (
composer.lock,package-lock.jsonи т.п.); - хранить конфигурацию среды (версии PHP, Node, расширения) в документации или в виде конфигов для конфигурационного менеджмента.
Когда имеет смысл перейти на полноценный CI/CD
Git‑деплой на VDS через bare‑репозиторий и hooks – отличный старт для небольших проектов. Но по мере роста требований и команды вы, скорее всего, упрётесь в ограничения:
- нужно тестировать код на разных версиях PHP/Node/БД;
- нужны миграции БД с откатами и проверками;
- нужно несколько VDS (прод, стейджинг, превью‑окружения на каждый merge request);
- нужен аудит деплоя: кто, когда, что выкатил.
Тогда уже имеет смысл строить полноценный pipeline в GitHub Actions или GitLab CI, использовать отдельного пользователя/сервис на VDS только для деплоя, учитывать миграции БД, состояние приложений и делать деплой атомарным и наблюдаемым.
Но фундамент всё равно будет тем же: git как источник истины, доступ по ключам, понятный скрипт деплоя и аккуратная работа с окружением на VDS.
Итоги
Мы разобрали практичную схему деплоя на VDS с помощью git, GitHub и GitLab:
- создали отдельного пользователя и bare‑репозиторий на VDS;
- настроили
post-receive‑hook для автоматического обновления кода; - показали, как интегрировать деплой с GitHub Actions и GitLab CI;
- обсудили релизные каталоги, безопасность SSH и типичные грабли.
Такой подход сохраняет простоту классического git‑деплоя, но даёт возможность масштабировать процесс: добавлять стейджинги, релизы, миграции и полноценный CI/CD, не меняя базовую архитектуру. А если вы только выбираете площадку под такой деплой, имеет смысл сразу смотреть в сторону надёжного хостинга с поддержкой VDS‑тарифов, где все эти практики можно реализовать без танцев с бубном.


