30.08.2026

Как внедрить CI/CD в проект: GitHub Actions, Docker и автоматический деплой

Ручной деплой хорошо работает на первых этапах разработки: разработчик подключается к серверу, загружает новую версию проекта, устанавливает зависимости и перезапускает приложение. Но по мере роста проекта такой процесс быстро становится источником ошибок.

Можно забыть выполнить миграцию, установить неправильную версию зависимости, развернуть код из другой ветки или просто пропустить один из шагов инструкции. Кроме того, каждый релиз начинает занимать время разработчика.

Решить эту проблему помогает CI/CD — автоматизированный процесс, при котором изменения проходят тестирование, сборку и доставку на сервер практически без ручных действий.

В простом варианте разработчику достаточно отправить изменения в основную ветку репозитория:

git push origin main

После этого CI/CD-система самостоятельно:

  1. получает исходный код;
  2. устанавливает зависимости;
  3. запускает тесты;
  4. собирает Docker-образ;
  5. публикует его в registry;
  6. подключается к серверу;
  7. скачивает новую версию приложения;
  8. перезапускает контейнер.

В этой статье построим такой pipeline с нуля. В качестве репозитория и CI/CD-платформы используем GitHub и GitHub Actions, приложение упакуем в Docker, Docker-образы будем хранить в Docker Hub, а production-среду развернем на Ubuntu VPS.

Итоговая архитектура будет выглядеть приблизительно так:

GitHub Repository

Исходный код проекта
Push / Pull Request

GitHub Actions

CI/CD Pipeline
Установка зависимостей
Тестирование
Docker Build

Docker Registry

Готовый образ приложения
SSH Deploy

Ubuntu VPS

Docker Compose + приложение

Production

Новая версия доступна пользователям
После отправки изменений в main весь путь от тестирования к production-деплою выполняется автоматически

Разверните сервер для CI/CD в Serverspace

Автоматический pipeline заканчивается там, где начинается production-инфраструктура. Поэтому для полноценного CI/CD понадобится сервер, постоянно доступный из интернета.

В Serverspace можно создать VPS в Казахстане и использовать его как production-среду для веб-приложения, API, backend-сервиса или другого проекта.

Для CI/CD удобно начать с виртуальной машины на Ubuntu. При создании сервера можно самостоятельно подобрать количество CPU, RAM и SSD, а затем масштабировать конфигурацию по мере роста нагрузки.

Такой VPS можно использовать для:

Для проекта из этой инструкции достаточно Ubuntu VPS с публичным IP-адресом и доступом по SSH.

Создайте VPS в Serverspace, выберите Ubuntu и используйте сервер как конечную точку автоматического deployment pipeline.

Что такое CI/CD

CI/CD объединяет несколько процессов автоматизации разработки.

CI — Continuous Integration, или непрерывная интеграция.

Ее задача — автоматически проверять изменения кода до того, как они попадут в production.

После каждого push или pull request система может:

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, который запускает нужную версию приложения.

Что понадобится перед началом

Для выполнения инструкции потребуется:

Для демонстрации далее будем использовать 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 и feature-веток.

Схема может выглядеть так:

feature/login

Feature-ветка с изменениями

Pull Request

Проверка изменений перед merge

CI: tests

Автоматический запуск тестов

main

Основная production-ветка

CI: tests + build

Повторная проверка и сборка приложения

CD: production deploy

Автоматическое развертывание в production
Feature-ветка сначала проходит тестирование через Pull Request, а после merge в main запускаются повторные проверки, сборка и production deployment

Таким образом, обычный 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"]

Здесь:

Конкретный 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-среду также можно разделить на несколько компонентов:

GitHub Actions

Serverspace VPS

Production infrastructure
↙ ↓ ↘
Application
Database
Monitoring
По мере роста проекта компоненты можно переносить на отдельные виртуальные машины и масштабировать независимо

Так 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: build

GitHub Actions создает SSH-конфигурацию и выполняет:

ssh "$SERVER_USER@$SERVER_HOST"

На удаленном сервере pipeline переходит в:

/opt/myapp

Записывает тег:

APP_TAG=$GITHUB_SHA

Затем выполняет:

docker compose --env-file .deploy.env pull

Docker скачивает новый образ.

После этого:

docker compose --env-file .deploy.env up -d --remove-orphans

Compose пересоздает контейнер с новой версией приложения.

В результате разработчику не нужно вручную подключаться к 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
npm ci
npm test

Если проверки завершились успешно, pull request можно объединить с main.

После merge:

main

Production-ветка репозитория

Tests

Автоматическая проверка кода

Docker Build

Сборка Docker-образа

Docker Hub

Публикация готового образа

SSH

Подключение к production-серверу

VPS

Production-инфраструктура

docker compose pull

Загрузка нового Docker-образа

docker compose up

Обновление и запуск контейнеров

Production

Новая версия приложения доступна пользователям
После изменения ветки main pipeline тестирует код, собирает и публикует Docker-образ, подключается к VPS и автоматически обновляет 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 nginx

Production-схема теперь выглядит так:

Internet

Входящий пользовательский трафик

Nginx :80 / :443

HTTP/HTTPS и reverse proxy

127.0.0.1:3000

Локальный порт приложения

Docker Container

Изолированная среда выполнения

Application

Backend или веб-приложение
Nginx принимает внешний HTTP/HTTPS-трафик и передает запросы на локальный порт Docker-контейнера, внутри которого работает приложение

Следующим шагом для production-сайта обычно становится настройка HTTPS.

Как сделать rollback

Автоматизация deployment не отменяет вероятность неудачного релиза.

Например:

Допустим, сейчас работает:

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-orphans

Production снова будет работать на предыдущей версии.

Именно поэтому 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/health

Endpoint может возвращать:

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-ветка

Production

Рабочая версия приложения
Изменения проходят CI и staging-проверку перед тем, как попасть из develop в main и быть развернутыми в production

Для этого можно использовать два сервера или два независимых окружения.

Например:

Ветка Среда Адрес
develop Staging staging.example.kz
main Production example.kz

Для каждого environment можно использовать отдельные SSH credentials и Secrets.

Добавьте ручное подтверждение production-деплоя

Continuous Deployment не всегда нужен.

Для критически важных проектов удобнее автоматически выполнять:

но требовать ручного approval перед production.

В GitHub Actions для этого можно создать отдельный Environment:

production

и назначить ему protection rules.

Затем добавить в deployment job:

environment:
name: production

Pipeline остановится перед production и продолжит работу после выполнения настроенных правил approval.

Это превращает Continuous Deployment в Continuous Delivery.

Не храните секреты в репозитории

Одна из наиболее серьезных ошибок при настройке CI/CD — хранение credentials непосредственно в YAML.

Нельзя делать так:

env:
SERVER_PASSWORD: "mypassword123"
API_KEY: "secret-api-key"
DOCKER_PASSWORD: "password"

Даже private repository не является подходящим хранилищем секретов.

Используйте:

Также не используйте один SSH-ключ одновременно для личного доступа разработчика и CI/CD.

Ограничьте права deployment-пользователя

Pipeline должен обладать только теми правами, которые действительно необходимы.

В нашем примере пользователю deploy требуется:

Давать CI/CD прямой root-доступ без необходимости не следует.

При более строгой модели безопасности можно дополнительно ограничить:

Важно учитывать, что членство в группе Docker фактически дает пользователю очень широкие возможности на сервере. Для инфраструктуры с повышенными требованиями к безопасности deployment-модель следует ограничивать еще сильнее.

Не деплойте при неуспешных тестах

Главное преимущество pipeline заключается именно в последовательной зависимости stages.

Правильная схема:

Tests

Автоматическая проверка приложения
Success
Failed →

Stop

Pipeline останавливается

Build

Сборка приложения или Docker-образа
Success
Failed →

Stop

Deployment не запускается

Deploy

Развертывание проверенной сборки
Каждый следующий этап CI/CD запускается только после успешного завершения предыдущего. Ошибка на стадии тестирования или сборки останавливает pipeline до deployment

Deployment никогда не должен запускаться независимо от результатов CI.

В GitHub Actions это обеспечивается через:

needs: test

и:

needs: build

Что еще можно добавить в CI

Наш пример запускает только тесты, но полноценный CI может включать значительно больше проверок.

Например:

Checkout

Получение исходного кода

Install Dependencies

Установка зависимостей проекта

Lint

Проверка качества и стиля кода

Unit Tests

Проверка отдельных компонентов

Integration Tests

Проверка взаимодействия компонентов

Security Scan

Поиск уязвимостей в коде и зависимостях

Docker Build

Сборка контейнерного образа

Container Scan

Проверка Docker-образа на уязвимости

Publish Artifact

Публикация проверенной сборки
Расширенный CI pipeline последовательно проверяет код, тестирует приложение, анализирует безопасность и только после успешного прохождения всех этапов публикует готовый артефакт

Для разных проектов сюда добавляют:

Чем раньше pipeline обнаруживает ошибку, тем дешевле ее исправить.

Что делать с миграциями базы данных

Если новая версия приложения требует изменения схемы базы данных, deployment становится сложнее.

Нельзя просто обновить контейнер, если новый код ожидает таблицу или колонку, которой еще нет.

Один из вариантов:

Build

Сборка новой версии приложения

Deploy Image

Доставка нового образа на сервер

Database Migration

Обновление схемы базы данных

Start Application

Запуск новой версии

Health Check

Проверка доступности и состояния приложения
При deployment с миграциями новая версия сначала доставляется на сервер, затем обновляется схема базы данных, запускается приложение и выполняется финальная проверка его работоспособности

Например, перед запуском новой версии можно выполнить:

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: false

GitHub не будет одновременно выполнять несколько jobs одной production-группы.

Это особенно важно, если pipeline содержит:

Как сократить downtime

Обычный:

docker compose up -d

может привести к небольшому промежутку между остановкой старого контейнера и готовностью нового.

Для небольших проектов это обычно допустимо.

Если приложение требует практически нулевого downtime, можно перейти к более сложным стратегиям.

Например:

Blue-Green Deployment

Users

Входящий пользовательский трафик

Load Balancer

Переключение трафика между версиями
↙ ↘

Blue v1

Текущая стабильная версия

Green v2

Новая версия приложения
При Blue-Green Deployment новая версия запускается параллельно со старой, а Load Balancer переключает трафик после проверки Green-среды

Новая версия запускается параллельно со старой.

После проверки балансировщик переключает пользователей на Green.

Если возникает проблема, трафик возвращается на Blue.

Другой вариант — Rolling Update, при котором экземпляры приложения заменяются постепенно.

Такие механизмы особенно часто используются вместе с Kubernetes.

Когда Docker Compose становится недостаточно

Docker Compose хорошо подходит для:

По мере роста инфраструктуры могут появиться требования:

В этом случае следующим этапом может стать Kubernetes.

При этом общая концепция CI/CD практически не меняется:

Git Repository

Исходный код проекта

CI

Тестирование и сборка

Docker Image

Готовый образ приложения

Container Registry

Хранение версий Docker-образов

CD

Автоматическое развертывание

Kubernetes Cluster

Запуск и управление контейнерами
CI собирает и публикует Docker-образ, после чего CD автоматически развертывает новую версию приложения в Kubernetes-кластере

Меняется в первую очередь последняя стадия deployment.

GitHub Actions — не единственный вариант

Архитектура статьи не привязана к GitHub.

Вместо GitHub Actions можно использовать:

В большинстве случаев workflow остается концептуально одинаковым:

Repository

Исходный код проекта

CI Runner

Выполнение CI/CD pipeline

Tests

Автоматическая проверка кода

Build Artifact

Сборка приложения или образа

Registry

Хранение готовых артефактов

Deployment

Доставка новой версии

Server

Работающая версия приложения
Универсальная схема CI/CD: от исходного кода до автоматического развертывания приложения на сервере

Поэтому важнее понимать саму архитектуру CI/CD, чем синтаксис конкретной платформы.

Типичные ошибки при настройке CI/CD

Permission denied (publickey)

GitHub Actions не может подключиться к серверу:

Permission denied (publickey)

Проверьте:

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

обычно означает:

Контейнер сразу завершается

Проверьте:

docker ps -a

Затем:

docker logs myapp

или:

docker compose --env-file .deploy.env logs app

Новый образ скачался, но сайт не работает

Проверьте:

Как должен развиваться CI/CD по мере роста проекта

Не обязательно сразу строить сложную DevOps-платформу.

Начать можно с простого pipeline:

Этап 1
Tests + SSH Deployment
Этап 2
Docker Registry + SHA Tags + Rollback
Этап 3
Staging + Production + Approval
Этап 4
Security Scans + Monitoring + Automated Rollback
Этап 5
Blue-Green / Kubernetes / Multi-node Infrastructure

Главное — автоматизировать повторяемый и уже понятный процесс.

Сложность pipeline должна расти вместе со сложностью самого проекта.

Заключение

CI/CD превращает deployment из последовательности ручных действий в воспроизводимый автоматический процесс.

В собранной нами архитектуре разработчику достаточно изменить код и отправить его в GitHub.

Дальше GitHub Actions:

  1. получает код;
  2. устанавливает зависимости;
  3. запускает тесты;
  4. создает Docker-образ;
  5. помечает его SHA текущего commit;
  6. отправляет образ в Docker Hub;
  7. подключается к production-серверу;
  8. скачивает новую версию;
  9. обновляет контейнер через Docker Compose.

Получается полный путь:

git push

Отправка изменений

GitHub

Репозиторий проекта

GitHub Actions

Запуск CI/CD pipeline

Tests

Автоматическая проверка кода

Docker Build

Сборка образа приложения

Docker Registry

Хранение готового образа

SSH Deployment

Подключение и обновление сервера

Ubuntu VPS

Production-сервер

Production

Новая версия доступна пользователям
Полный CI/CD-процесс: от отправки изменений в репозиторий до автоматического обновления приложения на production-сервере

Даже такой сравнительно простой pipeline уже дает несколько преимуществ:

Запустите production-среду для CI/CD в Serverspace

Для работы pipeline нужен постоянно доступный сервер, на котором будет запускаться готовая версия приложения.

В Serverspace можно создать облачный VPS на Ubuntu и самостоятельно настроить Docker, Nginx, базы данных и другие компоненты production-стека.

Начать можно с одного сервера:

GitHub Actions

CI/CD Pipeline

Serverspace VPS

Production-сервер

Nginx

Reverse proxy и HTTPS

Docker

Контейнерная среда

Application

Production-приложение

Database

Хранение данных
GitHub Actions автоматически обновляет приложение на VPS, где Nginx принимает внешний трафик, а Docker запускает приложение и связанные сервисы

А по мере роста проекта разделить инфраструктуру:

GitHub Actions

CI/CD Pipeline

Serverspace Cloud

Production и staging-инфраструктура

Application Server

Production-приложение

Database Server

База данных

Staging Server

Тестовая среда

Monitoring

Метрики и состояние сервисов
GitHub Actions автоматически доставляет новые версии приложения в облачную инфраструктуру Serverspace

Так одна и та же 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.