Как внедрить CI/CD в проект: GitHub Actions, Docker и автоматический деплой
Ручной деплой хорошо работает на первых этапах разработки: разработчик подключается к серверу, загружает новую версию проекта, устанавливает зависимости и перезапускает приложение. Но по мере роста проекта такой процесс быстро становится источником ошибок.
Можно забыть выполнить миграцию, установить неправильную версию зависимости, развернуть код из другой ветки или просто пропустить один из шагов инструкции. Кроме того, каждый релиз начинает занимать время разработчика.
Решить эту проблему помогает CI/CD — автоматизированный процесс, при котором изменения проходят тестирование, сборку и доставку на сервер практически без ручных действий.
В простом варианте разработчику достаточно отправить изменения в основную ветку репозитория:
git push origin mainПосле этого CI/CD-система самостоятельно:
- получает исходный код;
- устанавливает зависимости;
- запускает тесты;
- собирает Docker-образ;
- публикует его в registry;
- подключается к серверу;
- скачивает новую версию приложения;
- перезапускает контейнер.
В этой статье построим такой pipeline с нуля. В качестве репозитория и CI/CD-платформы используем GitHub и GitHub Actions, приложение упакуем в Docker, Docker-образы будем хранить в Docker Hub, а production-среду развернем на Ubuntu VPS.
Итоговая архитектура будет выглядеть приблизительно так:
GitHub Repository
GitHub Actions
Docker Registry
Ubuntu VPS
Production
Разверните сервер для CI/CD в Serverspace
Автоматический pipeline заканчивается там, где начинается production-инфраструктура. Поэтому для полноценного CI/CD понадобится сервер, постоянно доступный из интернета.
В Serverspace можно создать VPS в Казахстане и использовать его как production-среду для веб-приложения, API, backend-сервиса или другого проекта.
Для CI/CD удобно начать с виртуальной машины на Ubuntu. При создании сервера можно самостоятельно подобрать количество CPU, RAM и SSD, а затем масштабировать конфигурацию по мере роста нагрузки.
Такой VPS можно использовать для:
- Docker-контейнеров;
- backend-приложений;
- REST API;
- Node.js, Python, PHP и других runtime;
- Nginx;
- PostgreSQL и других баз данных;
- staging- и production-сред;
- self-hosted CI/CD-инструментов.
Для проекта из этой инструкции достаточно Ubuntu VPS с публичным IP-адресом и доступом по SSH.
Создайте VPS в Serverspace, выберите Ubuntu и используйте сервер как конечную точку автоматического deployment pipeline.
Что такое CI/CD
CI/CD объединяет несколько процессов автоматизации разработки.
CI — Continuous Integration, или непрерывная интеграция.
Ее задача — автоматически проверять изменения кода до того, как они попадут в production.
После каждого push или pull request система может:
- установить зависимости;
- проверить код линтером;
- запустить unit-тесты;
- запустить integration-тесты;
- собрать приложение;
- проверить, что Docker-образ создается без ошибок.
CD может расшифровываться как Continuous Delivery или Continuous Deployment.
Continuous Delivery означает, что готовая версия автоматически проходит все проверки и подготавливается к релизу, но сам запуск в production подтверждает человек.
При Continuous Deployment успешное изменение автоматически отправляется в production без дополнительного ручного шага.
Разница выглядит следующим образом:
| Подход | Что происходит автоматически | Деплой в production |
|---|---|---|
| Continuous Integration | Сборка, проверки и тестирование | Не обязательно |
| Continuous Delivery | Сборка готовой к релизу версии | После подтверждения |
| Continuous Deployment | Весь путь от commit до production | Автоматически |
В нашем примере реализуем последний вариант: после успешного merge или push в ветку main новая версия автоматически попадет на сервер.
Из каких компонентов будет состоять pipeline
Для примера используем пять основных компонентов.
GitHub
Git-репозиторий выступает источником кода и запускает pipeline после изменения ветки.
GitHub Actions
GitHub Actions будет выполнять CI/CD jobs на выделенном runner.
Именно здесь будут запускаться тесты, создаваться Docker-образ и выполняться deployment.
Docker
Docker позволяет упаковать приложение вместе с его runtime и зависимостями в переносимый образ.
Вместо ручной установки новой версии программы на сервер CI/CD будет просто заменять старый контейнер новым.
Docker Hub
Registry хранит собранные Docker-образы.
GitHub Actions отправляет туда новую версию, а production-сервер скачивает готовый образ.
При необходимости Docker Hub можно заменить на GitHub Container Registry, GitLab Container Registry или собственный registry.
Ubuntu VPS
На сервере работает Docker Compose, который запускает нужную версию приложения.
Что понадобится перед началом
Для выполнения инструкции потребуется:
- GitHub-репозиторий с проектом;
- учетная запись Docker Hub;
- Ubuntu VPS;
- доступ к серверу по SSH;
- Dockerfile для приложения;
- команда, запускающая тесты проекта.
Для демонстрации далее будем использовать Node.js-проект.
Но сама архитектура практически не зависит от языка.
Например, этап CI можно заменить следующим образом:
| Стек | Установка зависимостей | Тестирование |
|---|---|---|
| Node.js | npm ci | npm test |
| Python | pip install -r requirements.txt | pytest |
| PHP | composer install | phpunit |
| Go | go mod download | go test ./... |
После тестирования остальные этапы — Docker build, registry и deployment — могут оставаться практически одинаковыми.
Шаг 1. Подготовьте Git-репозиторий
Если проект еще не находится под контролем Git, инициализируйте репозиторий:
git initДобавьте файлы:
git add .
git commit -m "Initial commit"После создания удаленного репозитория GitHub подключите его:
git remote add origin [git@github.com](mailto:git@github.com):USERNAME/PROJECT.git
git branch -M main
git push -u origin mainЗамените USERNAME и PROJECT на имя аккаунта и репозитория.
Для CI/CD важно, чтобы production-деплой запускался только из контролируемой ветки.
Обычно для этого используют:
- main — production;
- develop — интеграционная ветка;
- feature-ветки — отдельные задачи.
В небольшом проекте достаточно main и feature-веток.
Схема может выглядеть так:
feature/login
Pull Request
CI: tests
main
CI: tests + build
CD: production deploy
Таким образом, обычный pull request запускает проверки, но не меняет production.
Шаг 2. Добавьте Dockerfile
В корне репозитория создайте файл:
DockerfileДля простого Node.js-приложения он может выглядеть следующим образом:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["npm", "start"]
Здесь:
- FROM определяет базовый образ;
- WORKDIR создает рабочую директорию;
- COPY переносит файлы проекта;
- npm ci устанавливает зависимости;
- EXPOSE документирует используемый порт;
- CMD запускает приложение.
Конкретный Dockerfile зависит от стека вашего проекта.
Для production-проекта также можно использовать multi-stage build, чтобы отделить этап компиляции приложения от итогового runtime-образа.
Шаг 3. Создайте .dockerignore
Не следует копировать в Docker-образ все содержимое рабочей директории.
Создайте:
.dockerignoreНапример:
node_modules
npm-debug.log
.git
.github
.env
.env.*
coverage
README.mdОсобенно важно исключить .env и другие файлы с секретами.
Пароли, API-ключи и production-конфигурация не должны попадать внутрь Docker-образа.
Шаг 4. Проверьте Docker-образ локально
Перед подключением CI/CD убедитесь, что проект собирается вручную.
Выполните:
docker build -t myapp:test .Запустите контейнер:
docker run --rm -p 3000:3000 myapp:testПосле этого приложение должно быть доступно на:
[http://localhost:3000(http://localhost:3000[/code)]
Если локальная Docker-сборка не работает, автоматизация проблему не исправит.
CI/CD должен повторять уже рабочий процесс сборки и deployment, а не заменять его.
Шаг 5. Подготовьте Ubuntu-сервер
Подключитесь к VPS:
ssh root@SERVER_IPОбновите систему:
apt update
apt upgrade -yУстановите необходимые пакеты:
apt install -y ca-certificates curlСоздайте директорию для ключей APT:
install -m 0755 -d /etc/apt/keyringsДобавьте ключ официального Docker-репозитория:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
Добавьте Docker repository:
cat > /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOFОбновите индекс пакетов:
apt updateУстановите Docker Engine и Compose:
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginПроверьте состояние Docker:
systemctl status dockerИ версию Compose:
docker compose versionШаг 6. Создайте отдельного пользователя для deployment
Не рекомендуется давать CI/CD постоянный SSH-доступ к серверу через пользователя root.
Создадим отдельного пользователя:
adduser deployДобавьте его в группу Docker:
usermod -aG docker deployСоздайте директорию приложения:
mkdir -p /opt/myapp
chown -R deploy:deploy /opt/myappТеперь выйдите из root-сессии и снова подключитесь как новый пользователь:
ssh deploy@SERVER_IPПроверьте Docker:
docker psЕсли команда работает без sudo, пользователь готов к deployment.
Шаг 7. Настройте SSH-ключ для GitHub Actions
GitHub Actions должен иметь возможность подключиться к серверу без ввода пароля.
На локальном компьютере создайте отдельную пару ключей:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f github_actions_deployБудут созданы два файла:
github_actions_deploy
github_actions_deploy.pubПервый — приватный ключ.
Второй — публичный.
Добавьте публичный ключ на сервер:
ssh-copy-id -i github_actions_deploy.pub deploy@SERVER_IPИли вручную добавьте его содержимое в:
/home/deploy/.ssh/authorized_keysПроверьте подключение:
ssh -i github_actions_deploy deploy@SERVER_IPПриватный ключ позднее добавим в GitHub Secrets.
Не сохраняйте его в Git-репозитории.
Создайте production-среду в Serverspace для автоматического deployment
После настройки pipeline сервер становится постоянной частью процесса разработки.
Вместо ручной загрузки каждой версии CI/CD автоматически подключается к VPS и обновляет контейнеры.
В Serverspace инфраструктуру можно подобрать под реальную нагрузку проекта: начать с небольшой виртуальной машины, а затем увеличить CPU, RAM или дисковое пространство по мере роста приложения.
Production-среду также можно разделить на несколько компонентов:
Serverspace VPS
Так pipeline можно сохранить даже при переходе от одного VPS к более сложной инфраструктуре.
Шаг 8. Создайте Docker Compose-конфигурацию на сервере
Перейдите в директорию проекта:
cd /opt/myappСоздайте файл:
compose.yamlДобавьте:
services:
app:
image: yourdockerhubuser/myapp:${APP_TAG:-latest}
container_name: myapp
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
env_file:
- app.envЗамените:
yourdockerhubuser/myappна имя своего Docker Hub repository.
Обратите внимание на:
${APP_TAG:-latest}Эта переменная позволит запускать не просто последнюю версию приложения, а конкретный Docker-образ, соответствующий определенному commit.
Это пригодится для rollback.
Шаг 9. Добавьте переменные окружения приложения
Production-секреты лучше хранить непосредственно на сервере, а не внутри Docker-образа.
Создайте:
nano /opt/myapp/app.envНапример:
NODE_ENV=production
DATABASE_URL=postgresql://user:password@database:5432/app
API_KEY=change_meОграничьте права:
chmod 600 /opt/myapp/app.envФайл app.env не требуется хранить в GitHub.
Так разработчик может собирать один и тот же Docker-образ и использовать разные настройки для staging и production.
Шаг 10. Настройте Docker Hub
Создайте repository в Docker Hub.
Например:
yourdockerhubuser/myappДля CI/CD лучше использовать access token вместо основного пароля учетной записи.
Если repository приватный, один раз авторизуйте production-сервер:
docker loginПосле успешной авторизации сервер сможет выполнять:
docker pull yourdockerhubuser/myapp:latestПри использовании публичного repository отдельная авторизация для скачивания обычно не требуется.
Шаг 11. Добавьте GitHub Secrets
Откройте GitHub repository и перейдите:
Settings
→ Secrets and variables
→ ActionsСоздайте следующие repository secrets:
| Secret | Содержимое |
|---|---|
| DOCKERHUB_USERNAME | Имя пользователя Docker Hub |
| DOCKERHUB_TOKEN | Access token Docker Hub |
| SERVER_HOST | IP-адрес или домен production-сервера |
| SERVER_USER | Например, deploy |
| SERVER_SSH_KEY | Содержимое приватного github_actions_deploy |
| SERVER_HOST_KEY | SSH host key сервера |
Получить host key можно командой:
ssh-keyscan -H SERVER_IPДобавьте получившуюся строку в SERVER_HOST_KEY.
Для production-среды желательно предварительно проверить fingerprint сервера через доверенный канал, а не автоматически принимать любой SSH host key непосредственно во время deployment.
Шаг 12. Создайте GitHub Actions workflow
В репозитории создайте директорию:
.github/workflowsВ ней создайте:
deploy.ymlДобавьте следующий workflow:
name: CI/CD
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
test:
name: Test application
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
build:
name: Build and push Docker image
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/myapp
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Login to Docker Hub
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
echo "$DOCKERHUB_TOKEN" | docker login \
-u "$DOCKERHUB_USERNAME" \
--password-stdin
- name: Build Docker image
run: |
docker build \
-t "$IMAGE_NAME:$GITHUB_SHA" \
-t "$IMAGE_NAME:latest" \
.
- name: Push Docker image
run: |
docker push "$IMAGE_NAME:$GITHUB_SHA"
docker push "$IMAGE_NAME:latest"
deploy:
name: Deploy to production
runs-on: ubuntu-latest
needs: build
concurrency:
group: production
cancel-in-progress: false
env:
SERVER_HOST: ${{ secrets.SERVER_HOST }}
SERVER_USER: ${{ secrets.SERVER_USER }}
SERVER_SSH_KEY: ${{ secrets.SERVER_SSH_KEY }}
SERVER_HOST_KEY: ${{ secrets.SERVER_HOST_KEY }}
steps:
- name: Configure SSH
run: |
mkdir -p ~/.ssh
printf '%s\n' "$SERVER_SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$SERVER_HOST_KEY" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Deploy application
run: |
ssh "$SERVER_USER@$SERVER_HOST" "
cd /opt/myapp &&
printf 'APP_TAG=%s\n' '$GITHUB_SHA' > .deploy.env &&
docker compose --env-file .deploy.env pull &&
docker compose --env-file .deploy.env up -d --remove-orphans
"
Это уже полноценный CI/CD pipeline.
Рассмотрим его подробнее.
Как работает секция on
Workflow запускается при двух событиях:
on:
pull_request:
branches:
- main
push:
branches:
- main
Pull request в main запускает CI.
Push в main запускает CI, а после него — build и deployment.
Это важное разделение.
Если разработчик просто открыл pull request, его код не должен автоматически попадать на production-сервер.
Как работает job test
Первый job:
testвыполняется при pull request и push.
GitHub создает временную виртуальную машину:
runs-on: ubuntu-latestЗатем получает код:
- uses: actions/checkout@v6Подготавливает Node.js:
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"Устанавливает зависимости:
npm ciИ запускает тесты:
npm testЕсли хотя бы одна команда завершится с ненулевым exit code, job получит статус failed.
Следующие этапы не запустятся.
Именно здесь CI защищает production от очевидно сломанной версии приложения.
Как работает Docker build
Следующий job зависит от успешного завершения тестов:
needs: testДополнительно используется условие:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'Поэтому Docker-образ публикуется только при реальном изменении main.
Pipeline выполняет авторизацию:
echo "$DOCKERHUB_TOKEN" | docker login
-u "$DOCKERHUB_USERNAME"
--password-stdinПосле этого создает сразу два тега:
docker build
-t "$IMAGE_NAME:$GITHUB_SHA"
-t "$IMAGE_NAME:latest"
.Например, registry может содержать:
myapp:latest
myapp:ac34a0f7...
myapp:c014d9b2...
myapp:8bfc2201...latest удобно показывает последнюю сборку.
SHA-теги позволяют однозначно связать Docker-образ с конкретным Git commit.
Именно SHA будем использовать для production.
Почему лучше деплоить SHA, а не только latest
Рассмотрим ситуацию.
В понедельник развернут commit:
1a2b3cВо вторник:
4d5e6fПосле второго deployment обнаружилась ошибка.
Если сервер использует только:
myapp:latestне всегда очевидно, какая именно версия приложения работала до обновления.
При использовании SHA:
myapp:1a2b3c
myapp:4d5e6fверсию можно определить однозначно.
Поэтому workflow записывает SHA текущего commit в:
/opt/myapp/.deploy.envНапример:
APP_TAG=4d5e6f...Docker Compose использует этот тег здесь:
image: yourdockerhubuser/myapp:${APP_TAG:-latest}Так production-среда всегда связана с конкретным commit.
Как работает deployment по SSH
Последний job выполняется только после успешного build:
needs: buildGitHub Actions создает SSH-конфигурацию и выполняет:
ssh "$SERVER_USER@$SERVER_HOST"На удаленном сервере pipeline переходит в:
/opt/myappЗаписывает тег:
APP_TAG=$GITHUB_SHAЗатем выполняет:
docker compose --env-file .deploy.env pullDocker скачивает новый образ.
После этого:
docker compose --env-file .deploy.env up -d --remove-orphansCompose пересоздает контейнер с новой версией приложения.
В результате разработчику не нужно вручную подключаться к production.
Полный путь одного изменения
Теперь рассмотрим весь workflow целиком.
Разработчик создает ветку:
git checkout -b feature/new-apiВносит изменения и отправляет их:
git add .
git commit -m "Add new API endpoint"
git push origin feature/new-apiПосле создания pull request:
Если проверки завершились успешно, pull request можно объединить с main.
После merge:
main
Tests
Docker Build
Docker Hub
SSH
VPS
docker compose pull
docker compose up
Production
Все действия после merge происходят автоматически.
Шаг 13. Проверьте первый deployment
После merge откройте вкладку:
GitHub
→ Actions
→ CI/CDВы увидите три jobs:
Test application
Build and push Docker image
Deploy to productionОни должны последовательно получить статус success.
После этого подключитесь к серверу:
ssh deploy@SERVER_IPПроверьте контейнер:
docker psВывод будет приблизительно таким:
CONTAINER ID IMAGE STATUS
5ae62b43d810 yourdockerhubuser/myapp:abc123 Up 1 minuteПосмотрите сохраненный deployment tag:
cat /opt/myapp/.deploy.envНапример:
APP_TAG=abc123...Посмотреть логи приложения можно через:
cd /opt/myapp
docker compose --env-file .deploy.env logs -fКак подключить Nginx
Обычно production-приложение не публикуют напрямую на внешнем интерфейсе через порт Node.js.
В нашей Compose-конфигурации используется:
127.0.0.1:3000:3000Поэтому порт 3000 доступен только локально на сервере.
Перед приложением можно поставить Nginx.
Установите его:
sudo apt install nginxСоздайте конфигурацию:
sudo nano /etc/nginx/sites-available/myappПример:
server {
listen 80;
server_name example.kz [www.example.kz](http://www.example.kz);
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Активируйте ее:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myappПроверьте:
sudo nginx -tПерезагрузите конфигурацию:
sudo systemctl reload nginxProduction-схема теперь выглядит так:
Internet
Nginx :80 / :443
127.0.0.1:3000
Docker Container
Application
Следующим шагом для production-сайта обычно становится настройка HTTPS.
Как сделать rollback
Автоматизация deployment не отменяет вероятность неудачного релиза.
Например:
- тесты не покрыли определенный сценарий;
- изменился внешний API;
- ошибка проявилась только на production-данных;
- возникла проблема производительности.
Допустим, сейчас работает:
APP_TAG=4d5e6fА предыдущая стабильная версия:
APP_TAG=1a2b3cДля rollback подключитесь к серверу:
ssh deploy@SERVER_IPПерейдите в директорию:
cd /opt/myappУкажите предыдущий тег:
echo "APP_TAG=1a2b3c" > .deploy.envСкачайте образ:
docker compose --env-file .deploy.env pullПерезапустите приложение:
docker compose --env-file .deploy.env up -d --remove-orphansProduction снова будет работать на предыдущей версии.
Именно поэтому immutable-теги по commit SHA удобнее использования только latest.
Добавьте health check после deployment
Успешный запуск контейнера еще не гарантирует, что приложение действительно отвечает на запросы.
После deployment можно добавить автоматическую проверку endpoint.
Например, если приложение предоставляет:
https://example.kz/healthдобавьте в workflow после SSH deployment:
- name: Verify deployment
run: |
sleep 10
curl --fail --silent --show-error
https://example.kz/healthEndpoint может возвращать:
HTTP/1.1 200 OKи, например:
{
"status": "ok"
}Если curl получает ошибку, GitHub Actions отметит job как failed.
Для более зрелого pipeline этот этап можно связать с автоматическим rollback.
Разделите staging и production
Деплой каждого изменения сразу в production подходит не всем проектам.
Более безопасная схема:
Feature Branch
Pull Request
CI
develop
Staging
Verification
main
Production
Для этого можно использовать два сервера или два независимых окружения.
Например:
| Ветка | Среда | Адрес |
|---|---|---|
| develop | Staging | staging.example.kz |
| main | Production | example.kz |
Для каждого environment можно использовать отдельные SSH credentials и Secrets.
Добавьте ручное подтверждение production-деплоя
Continuous Deployment не всегда нужен.
Для критически важных проектов удобнее автоматически выполнять:
- тестирование;
- сборку;
- публикацию Docker-образа;
но требовать ручного approval перед production.
В GitHub Actions для этого можно создать отдельный Environment:
productionи назначить ему protection rules.
Затем добавить в deployment job:
environment:
name: productionPipeline остановится перед production и продолжит работу после выполнения настроенных правил approval.
Это превращает Continuous Deployment в Continuous Delivery.
Не храните секреты в репозитории
Одна из наиболее серьезных ошибок при настройке CI/CD — хранение credentials непосредственно в YAML.
Нельзя делать так:
env:
SERVER_PASSWORD: "mypassword123"
API_KEY: "secret-api-key"
DOCKER_PASSWORD: "password"Даже private repository не является подходящим хранилищем секретов.
Используйте:
- GitHub Secrets;
- Environment Secrets;
- secret manager;
- production .env с ограниченными правами;
- отдельные credentials для CI/CD.
Также не используйте один SSH-ключ одновременно для личного доступа разработчика и CI/CD.
Ограничьте права deployment-пользователя
Pipeline должен обладать только теми правами, которые действительно необходимы.
В нашем примере пользователю deploy требуется:
- SSH-доступ;
- доступ к /opt/myapp;
- возможность управлять Docker.
Давать CI/CD прямой root-доступ без необходимости не следует.
При более строгой модели безопасности можно дополнительно ограничить:
- SSH-команды;
- сетевой доступ;
- sudo;
- доступ к другим директориям;
- доступ к production database.
Важно учитывать, что членство в группе Docker фактически дает пользователю очень широкие возможности на сервере. Для инфраструктуры с повышенными требованиями к безопасности deployment-модель следует ограничивать еще сильнее.
Не деплойте при неуспешных тестах
Главное преимущество pipeline заключается именно в последовательной зависимости stages.
Правильная схема:
Tests
Stop
Build
Stop
Deploy
Deployment никогда не должен запускаться независимо от результатов CI.
В GitHub Actions это обеспечивается через:
needs: testи:
needs: buildЧто еще можно добавить в CI
Наш пример запускает только тесты, но полноценный CI может включать значительно больше проверок.
Например:
Checkout
Install Dependencies
Lint
Unit Tests
Integration Tests
Security Scan
Docker Build
Container Scan
Publish Artifact
Для разных проектов сюда добавляют:
- ESLint;
- Prettier;
- TypeScript compilation;
- pytest;
- PHPUnit;
- SonarQube;
- SAST;
- dependency scanning;
- Docker image scanning;
- проверку лицензий зависимостей.
Чем раньше pipeline обнаруживает ошибку, тем дешевле ее исправить.
Что делать с миграциями базы данных
Если новая версия приложения требует изменения схемы базы данных, deployment становится сложнее.
Нельзя просто обновить контейнер, если новый код ожидает таблицу или колонку, которой еще нет.
Один из вариантов:
Build
Deploy Image
Database Migration
Start Application
Health Check
Например, перед запуском новой версии можно выполнить:
docker compose run --rm app npm run migrateДля Python-проекта:
docker compose run --rm app python manage.py migrateНо production-миграции нужно проектировать особенно внимательно.
Желательно, чтобы изменение схемы оставалось совместимым как с новой, так и с предыдущей версией приложения хотя бы во время переходного периода.
Иначе простой rollback контейнера может оказаться невозможным.
Как избежать одновременного deployment двух версий
Представим, что два commit попали в main почти одновременно.
Без дополнительной защиты могут запуститься два deployment jobs.
Для этого в нашем workflow используется:
concurrency:
group: production
cancel-in-progress: falseGitHub не будет одновременно выполнять несколько jobs одной production-группы.
Это особенно важно, если pipeline содержит:
- миграции;
- долгие deployment scripts;
- перезапуск нескольких сервисов;
- операции с базой данных.
Как сократить downtime
Обычный:
docker compose up -dможет привести к небольшому промежутку между остановкой старого контейнера и готовностью нового.
Для небольших проектов это обычно допустимо.
Если приложение требует практически нулевого downtime, можно перейти к более сложным стратегиям.
Например:
Blue-Green Deployment
Users
Load Balancer
Blue v1
Green v2
Новая версия запускается параллельно со старой.
После проверки балансировщик переключает пользователей на Green.
Если возникает проблема, трафик возвращается на Blue.
Другой вариант — Rolling Update, при котором экземпляры приложения заменяются постепенно.
Такие механизмы особенно часто используются вместе с Kubernetes.
Когда Docker Compose становится недостаточно
Docker Compose хорошо подходит для:
- небольших production-проектов;
- личных сервисов;
- MVP;
- backend-приложений;
- нескольких связанных контейнеров;
- staging-сред.
По мере роста инфраструктуры могут появиться требования:
- несколько application nodes;
- автоматическое восстановление;
- rolling updates;
- autoscaling;
- service discovery;
- распределение нагрузки;
- централизованное управление Secrets.
В этом случае следующим этапом может стать Kubernetes.
При этом общая концепция CI/CD практически не меняется:
Git Repository
CI
Docker Image
Container Registry
CD
Kubernetes Cluster
Меняется в первую очередь последняя стадия deployment.
GitHub Actions — не единственный вариант
Архитектура статьи не привязана к GitHub.
Вместо GitHub Actions можно использовать:
- GitLab CI/CD;
- Jenkins;
- TeamCity;
- CircleCI;
- Bitbucket Pipelines;
- Azure Pipelines;
- self-hosted runners.
В большинстве случаев workflow остается концептуально одинаковым:
Repository
CI Runner
Tests
Build Artifact
Registry
Deployment
Server
Поэтому важнее понимать саму архитектуру CI/CD, чем синтаксис конкретной платформы.
Типичные ошибки при настройке CI/CD
Permission denied (publickey)
GitHub Actions не может подключиться к серверу:
Permission denied (publickey)Проверьте:
- содержимое SERVER_SSH_KEY;
- наличие публичного ключа в authorized_keys;
- имя пользователя;
- права директории ~/.ssh;
- порт SSH.
Permission denied при работе с Docker
Например:
permission denied while trying to connect to the Docker daemon socketУбедитесь, что deployment-пользователь входит в группу:
dockerПроверить можно командой:
groups deployПосле добавления пользователя в группу требуется новая login-сессия.
pull access denied
Ошибка:
pull access denied for myappобычно означает:
- неправильное имя image;
- приватный repository;
- отсутствие Docker login на сервере;
- неверные registry credentials.
Контейнер сразу завершается
Проверьте:
docker ps -aЗатем:
docker logs myappили:
docker compose --env-file .deploy.env logs appНовый образ скачался, но сайт не работает
Проверьте:
- логи контейнера;
- порт приложения;
- Nginx;
- переменные окружения;
- подключение к базе данных;
- Firewall;
- health endpoint.
Как должен развиваться CI/CD по мере роста проекта
Не обязательно сразу строить сложную DevOps-платформу.
Начать можно с простого pipeline:
Главное — автоматизировать повторяемый и уже понятный процесс.
Сложность pipeline должна расти вместе со сложностью самого проекта.
Заключение
CI/CD превращает deployment из последовательности ручных действий в воспроизводимый автоматический процесс.
В собранной нами архитектуре разработчику достаточно изменить код и отправить его в GitHub.
Дальше GitHub Actions:
- получает код;
- устанавливает зависимости;
- запускает тесты;
- создает Docker-образ;
- помечает его SHA текущего commit;
- отправляет образ в Docker Hub;
- подключается к production-серверу;
- скачивает новую версию;
- обновляет контейнер через Docker Compose.
Получается полный путь:
git push
GitHub
GitHub Actions
Tests
Docker Build
Docker Registry
SSH Deployment
Ubuntu VPS
Production
Даже такой сравнительно простой pipeline уже дает несколько преимуществ:
- одинаковый процесс deployment при каждом релизе;
- автоматическую проверку кода;
- меньше ручных операций;
- историю deployment;
- связь production-версии с Git commit;
- возможность быстрого rollback;
- основу для дальнейшего перехода к staging, Kubernetes и более сложной DevOps-инфраструктуре.
Запустите production-среду для CI/CD в Serverspace
Для работы pipeline нужен постоянно доступный сервер, на котором будет запускаться готовая версия приложения.
В Serverspace можно создать облачный VPS на Ubuntu и самостоятельно настроить Docker, Nginx, базы данных и другие компоненты production-стека.
Начать можно с одного сервера:
GitHub Actions
Serverspace VPS
Nginx
Docker
Application
Database
А по мере роста проекта разделить инфраструктуру:
GitHub Actions
Serverspace Cloud
Application Server
Database Server
Staging Server
Monitoring
Так одна и та же CI/CD-модель может использоваться как для небольшого проекта, так и для более сложной инфраструктуры.
Создайте VPS в Serverspace и настройте автоматический путь от commit в репозитории до работающей версии приложения на сервере.
FAQ
Что такое CI/CD простыми словами?
CI/CD — это автоматизация проверки, сборки и доставки кода. После изменения репозитория система может самостоятельно запустить тесты, собрать приложение и развернуть новую версию на сервере. Благодаря этому разработчику не приходится повторять одни и те же операции вручную при каждом релизе.
Обязательно ли использовать Docker для CI/CD?
Нет. CI/CD можно настроить и без Docker: например, копировать исходный код на сервер и перезапускать systemd-сервис. Docker делает deployment более воспроизводимым, поскольку приложение, runtime и зависимости упаковываются в единый образ, который можно одинаково запускать в разных средах.
Можно ли настроить CI/CD на обычном VPS?
Да. Для небольшого и среднего проекта достаточно обычного Linux VPS с SSH-доступом. CI/CD-система может подключаться к серверу, скачивать новую версию приложения и перезапускать Docker-контейнеры или системные сервисы. Для описанной в статье схемы подойдет Ubuntu VPS с Docker и Docker Compose.
Чем GitHub Actions отличается от Jenkins и GitLab CI/CD?
GitHub Actions тесно интегрирован с GitHub и позволяет хранить workflow непосредственно в репозитории. GitLab CI/CD выполняет похожую функцию внутри GitLab. Jenkins — отдельная self-hosted система автоматизации с широкими возможностями настройки. Общая архитектура pipeline при этом остается похожей: получение кода, тестирование, сборка и deployment.
Как откатить неудачный deployment?
Удобнее всего публиковать Docker-образы не только с тегом latest, но и с уникальным тегом, например SHA Git commit. Если новая версия работает неправильно, на сервере можно указать SHA предыдущего стабильного образа и повторно выполнить docker compose pull и docker compose up.
Нужно ли хранить пароли сервера в GitHub Actions?
Обычные пароли и ключи нельзя записывать непосредственно в workflow или исходный код. Для credentials следует использовать GitHub Secrets или Environment Secrets. Для подключения к серверу предпочтительнее отдельный SSH-ключ deployment-пользователя, а production-переменные приложения можно хранить отдельно на сервере или в специализированном secret manager.