Зачем нужен CI/CD на VPS
CI/CD (Continuous Integration / Continuous Deployment) автоматизирует сборку, тестирование и развёртывание приложений. Вместо ручного деплоя по SSH каждый push в main-ветку запускает pipeline, который собирает Docker-образ, прогоняет тесты и обновляет приложение на VPS без простоя.
GitHub Actions встроен в сам GitHub, поэтому отдельный сервис заводить не нужно: по тарифам GitHub бесплатный лимит — 2 000 минут в месяц для приватных репозиториев, у публичных лимита нет. Плюс self-hosted runners на своём VPS и интеграция с Docker.
Что даёт CI/CD разработчику
- Автоматический деплой — push в main обновляет production без ручных действий.
- Раннее обнаружение ошибок — тесты прогоняются автоматически при каждом push, до попадания на сервер.
- Воспроизводимость сборки — один и тот же Docker-образ запускается на dev и production, поэтому окружение приложения совпадает.
- Откат — если образы помечены тегом по коммиту, предыдущую версию запускают одной командой; без тегов docker image prune из примеров ниже удаляет старый образ, и откат — это пересборка прошлого коммита.
- Командная работа — каждый PR проходит проверки, merge в main автоматически деплоит.
Требования к VPS для CI/CD
Для комфортной работы CI/CD pipeline на VPS необходимы ресурсы для Docker-сборки и запуска self-hosted runner.
Минимальная конфигурация (1 проект)
- CPU: 2 vCPU
- RAM: 4 ГБ
- Диск: 50 ГБ NVMe SSD
- ОС: Ubuntu 22.04 или 24.04 LTS
- Docker + Docker Compose
- Статический IP-адрес
Для нескольких проектов
- CPU: 4 vCPU
- RAM: 8 ГБ
- Диск: 100 ГБ NVMe SSD
- Параллельные runners для разных репозиториев
Рекомендуемые провайдеры: Timeweb Cloud — Cloud MSK 50 за 1 080 ₽/мес при оплате за 12 месяцев со скидкой 10 %: 2 vCPU, 4 ГБ, 50 ГБ NVMe — ровно минимум выше. Списание почасовое. Младший MSK 40 дешевле, но там 2 ГБ памяти и 40 ГБ диска, то есть под этот минимум он не проходит. AdminVPS — Micro за 899 ₽/мес при оплате за месяц: 2 ядра 3,6 ГГц, 4 ГБ, 60 ГБ NVMe. Младшие Lite и Promo — 299 и 499 ₽/мес при оплате за месяц — под минимум выше не проходят: у них одно ядро и 1–2 ГБ памяти.
Реклама · ООО «Админвпс»
AdminVPS Micro — 899 ₽ при оплате за месяц
2 ядра 3,6 ГГц, 4 ГБ памяти, 60 ГБ NVMe, канал до 1 Гбит/с и 5 ТБ включённого трафика — это ровно минимальная конфигурация под один проект из списка выше, с запасом по диску: Docker-сборки и слои образов набираются быстро, и пятьдесят гигабайт кончаются раньше, чем ожидаешь. Self-hosted runner живёт на том же сервере. Под несколько проектов с параллельными runner'ами понадобится Start за 1 289 ₽ при оплате за месяц: 4 ядра и 8 ГБ. Резервные копии еженедельные и входят в тариф — для сервера, куда деплоит pipeline, это не мелочь. При оплате за 12 месяцев Micro стоит 9 170 ₽ за год вместо 10 788.
Год −15 %
Промокод EASYLINK — скидка 60 % на первый месяц при оплате за 1 месяц, кроме тарифов Lite.
Подготовка VPS: установка Docker и Docker Compose
Все команды актуальны для Ubuntu 22.04/24.04 по состоянию на 2026 год. Скрипт get.docker.com — быстрый путь для тестового сервера; для боевого документация Docker советует ставить Docker из его apt-репозитория (раздел Install Docker Engine on Ubuntu на docs.docker.com).
Установка Docker
sudo apt update && sudo apt upgrade -y
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
docker --versionУстановка Docker Compose
# Скрипт get.docker.com уже поставил плагин Compose — достаточно проверить:
docker compose version
# Если плагина нет (Docker ставили иначе), он ставится из репозитория Docker:
# sudo apt install docker-compose-plugin -yСоздание пользователя для деплоя
# Отдельный пользователь для безопасности
sudo useradd -m -s /bin/bash deployer
sudo usermod -aG docker deployer
# SSH-ключ для GitHub Actions
sudo su - deployer
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_deploy_key -N ""
cat ~/.ssh/github_deploy_key.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
cat ~/.ssh/github_deploy_key # Скопировать в GitHub SecretsНастройка GitHub Actions: SSH-деплой
Самый простой способ CI/CD — GitHub Actions деплоит на VPS через SSH. Не требует self-hosted runner.
Настройка секретов в GitHub
- Откройте репозиторий на GitHub
- Settings - Secrets and variables - Actions
- Добавьте секреты: VPS_HOST (IP сервера), VPS_USER (deployer), SSH_PRIVATE_KEY (приватный ключ), SSH_KNOWN_HOSTS (результат ssh-keyscan -H VPS_IP)
Workflow для Node.js приложения
# .github/workflows/deploy.yml
name: Deploy to VPS
on:
push:
branches: [main]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Deploy via SSH
run: |
ssh ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} << 'EOF'
cd /app/myproject
git pull origin main
docker compose build
docker compose up -d --remove-orphans
docker image prune -f
EOFПри каждом push в main: GitHub подключается к VPS по SSH, обновляет код через git pull, пересобирает Docker-образ и перезапускает контейнеры. Репозиторий на сервере клонируют заранее, приватный — с ключом развёртывания (Deploy key), который по умолчанию даёт только чтение.
Каталог и .env — до первого деплоя. У deployer нет sudo, и создать /app он не может: на проверке mkdir от его имени дал Permission denied. Каталог создаёт администратор, клонирует уже deployer, а файл .env с паролями кладут рядом с docker-compose.yml — в репозиторий он не попадает:
sudo mkdir -p /app && sudo chown deployer:deployer /app
sudo -iu deployer git clone АДРЕС_РЕПОЗИТОРИЯ /app/myproject
# затем /app/myproject/.env — что в нём, сказано у примера docker-compose.yml нижеSelf-hosted runner на VPS
Self-hosted runner — агент GitHub Actions, запущенный на вашем VPS. Билды выполняются локально, что быстрее и без лимитов на минуты.
Установка runner
# Создаём пользователя для runner
sudo useradd -m -s /bin/bash github-runner
sudo usermod -aG docker github-runner
sudo -iu github-runner
# Скачиваем runner (номер версии и ссылка — в Settings - Actions - Runners - New)
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.338.0.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.338.0/actions-runner-linux-x64-2.338.0.tar.gz
tar xzf actions-runner-linux-x64-2.338.0.tar.gz
# Настройка (токен из GitHub Settings - Actions - Runners - New)
./config.sh --url https://github.com/YOUR/REPO --token YOUR_TOKEN
exit
# Службу ставит администратор с sudo — от имени github-runner
cd /home/github-runner/actions-runner
sudo ./svc.sh install github-runner
sudo ./svc.sh start
sudo ./svc.sh statusWorkflow с self-hosted runner
# .github/workflows/deploy.yml
name: Build & Deploy
on:
push:
branches: [main]
permissions:
contents: read
jobs:
build-and-deploy:
runs-on: self-hosted # Ваш runner на VPS
steps:
- uses: actions/checkout@v7
- name: Build Docker image
run: docker compose build
- name: Run tests
run: docker compose run --rm app npm test
- name: Deploy
run: |
docker compose up -d --remove-orphans # пересоздаст только изменившиеся сервисы
docker image prune -f
- name: Health check
run: |
sleep 10
curl -f http://localhost:3000/health || exit 1Self-hosted runner экономит время: нет задержки на запуск hosted runner и скачивание зависимостей. Docker-кеш хранится на VPS между запусками.
Docker Compose для production
Правильно настроенный docker-compose.yml — основа надёжного CI/CD. Включает healthcheck, restart policy, volumes и ограничения ресурсов.
Пример для Node.js
# docker-compose.yml
services:
app:
build: .
ports:
- "127.0.0.1:3000:3000" # наружу — через Nginx: опубликованный порт идёт мимо ufw
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"] # curl нужен в образе: в node:22-alpine и node:22-slim его нет
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
depends_on:
db:
condition: service_healthy
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
pgdata:Без двух вещей этот пример не поднимется. Первая — curl в образе: на проверке контейнер на node:22-alpine без него так и остался unhealthy (curl: executable file not found), хотя приложение отвечало. В Dockerfile на alpine хватает строки:
RUN apk add --no-cache curlВторая — файл .env рядом с docker-compose.yml. Без него переменные пустые: Postgres перезапускается по кругу с ошибкой «superuser password is not specified», а app не стартует вовсе — Compose ждёт здоровую базу.
DB_PASSWORD=придумайте-пароль
DATABASE_URL=postgres://postgres:придумайте-пароль@db:5432/postgresПример для Python (FastAPI/Django)
# docker-compose.yml
services:
web:
build:
context: .
dockerfile: Dockerfile
# pip install uvicorn-worker; для Django вместо main:app — myproject.asgi:application
command: gunicorn main:app -w 4 -k uvicorn_worker.UvicornWorker -b 0.0.0.0:8000
ports:
- "127.0.0.1:8000:8000"
environment:
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=${SECRET_KEY}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
depends_on:
redis:
condition: service_healthy
redis:
image: redis:7-alpine
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10sВ python:3.12-slim curl тоже нет: без него проверка здоровья всегда unhealthy, хотя /health отвечает 200. В Dockerfile — до установки зависимостей:
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
Zero-downtime деплой
Стандартный docker compose down && up вызывает простой: сервис недоступен, пока новый контейнер не запустится. Для production-сервисов это неприемлемо.
Пересборка одного сервиса
# Пересобрать и перезапустить только app, не трогая базу
docker compose up -d --no-deps --build app
# Compose сперва останавливает старый контейнер, потом запускает новый:
# простой короче, чем у down && up, но он естьНа нашей проверке 13.10.2026 (2 vCPU, 2 ГБ памяти) пересборка app этой командой заняла 11,6 секунды, а ответа не было около 0,5 секунды: при опросе каждые 0,1 секунды не дошли 5 запросов из 193. Маленькое приложение — у тяжёлого старт дольше, и окно шире.
Blue-Green деплой
# Два профиля: blue и green
# docker-compose.yml
services:
app-blue:
build: .
ports: ["127.0.0.1:3001:3000"]
profiles: ["blue"]
app-green:
build: .
ports: ["127.0.0.1:3002:3000"]
profiles: ["green"]
# Nginx переключает upstream:
# upstream app { server 127.0.0.1:3001; } # blue
# upstream app { server 127.0.0.1:3002; } # greenBlue-Green подход: деплоите новую версию параллельно, проверяете health, переключаете Nginx upstream, останавливаете старую версию. Нулевой простой.
Безопасность CI/CD pipeline
CI/CD pipeline имеет доступ к серверу и секретам. Безопасность критически важна.
- Минимальные права — отдельный пользователь deployer без sudo. Но группа docker, по документации Docker, даёт права уровня root, поэтому ключ deployer берегут как ключ root.
- SSH-ключи вместо паролей — ключ Ed25519, а ограничение по IP в authorized_keys — когда адрес раннера постоянный: у своего раннера или у больших раннеров GitHub со статическим IP. Адреса обычных раннеров GitHub меняются, и GitHub не советует вносить их в списки доступа.
- Ротация секретов — обновляйте SSH-ключи и токены каждые 90 дней.
- Ограничение self-hosted runner — не используйте на production-сервере для публичных репозиториев.
- Сканирование образов — docker scout cves перед деплоем. Плагин Scout ставится отдельно: после скрипта get.docker.com команды docker scout на сервере нет.
- GITHUB_TOKEN scope — минимальные permissions в workflow.
# Ограничение SSH по IP в authorized_keys (раннер с постоянным адресом)
from="203.0.113.0/24" ssh-ed25519 AAAA... github-actionsПроверка на сервере 13.10.2026: что работает по шагам
Наш замер 13.10.2026. На чистом сервере Timeweb Cloud (2 vCPU, 2 ГБ памяти) с Ubuntu 24.04 мы прошли разделы статьи по порядку: Docker из get.docker.com (29.9.0, Compose v5.6.0 уже внутри), пользователь deployer с ключом, SSH-деплой из workflow, оба примера docker-compose.yml, пересборку одного сервиса, профили blue-green и скачивание раннера.
- Шаг Deploy via SSH из workflow сработал дословно: git pull, сборка и перезапуск заняли 13 секунд, на сервере новая версия. Но только после двух шагов, которых в статье не было, — каталога /app от администратора и файла .env; оба добавлены выше.
- Пример для Node.js без .env не поднялся вовсе, с .env — поднялся, но проверка здоровья через curl в node:22-alpine давала unhealthy. С curl в образе — healthy через 32 секунды. В Python-примере то же с python:3.12-slim; команда gunicorn с uvicorn_worker.UvicornWorker верна — четыре воркера, около 114 МБ памяти.
- Опубликованный порт идёт мимо ufw — подтвердилось: при ufw, где открыт только SSH, контейнер с -p 8081:80 отвечал снаружи, а с -p 127.0.0.1:8082:80 — нет.
- Без SSH_KNOWN_HOSTS подключение обрывается на «Host key verification failed» — секрет нужен.
Чего проверка не показывает: сам GitHub — секреты, запуск workflow на его раннерах и регистрацию своего раннера (для неё нужен токен репозитория). Роль GitHub играл сам сервер: репозиторий-источник лежал на нём же, ключ и known_hosts — те же, что ушли бы в секреты. Приложения — наши пустые, а не ваш проект.
Заключение
Если сред нужно много — по одной на проект и на неделю-другую, — считать стоит не мощность, а время жизни: у почасового списания среда, прожившая неделю, обходится примерно в четверть месячного тарифа. Расчёт для агентства на пятнадцать проектов — в отдельном разборе.
CI/CD на VPS с GitHub Actions и Docker — это инвестиция в скорость и надёжность разработки. SSH-деплой подходит для простых проектов, self-hosted runner — для команд с частыми деплоями. Docker Compose обеспечивает воспроизводимость, а blue-green деплой — нулевой простой.
Для запуска CI/CD достаточно VPS с 2 vCPU и 4 ГБ RAM: 899 ₽/мес при оплате за месяц у AdminVPS (тариф Micro) или 1 080 ₽/мес при оплате за 12 месяцев у Timeweb Cloud (MSK 50). Сравните тарифы провайдеров с Docker-поддержкой на EasyLinkLife.
Реклама · АО «ТаймВэб»
VPS для CI/CD с Docker
Cloud MSK 50 — 2 vCPU, 4 ГБ RAM, 50 ГБ NVMe: 1 200 ₽/мес при оплате за месяц или 1 080 ₽/мес при оплате за 12 месяцев. Списание почасовое, Docker, GitLab и Portainer ставятся готовыми образами из маркетплейса.
Реклама · ООО «Админвпс»
AdminVPS — сервер под self-hosted runner
Под сборку образов и прогон тестов берите Start: 4 vCPU, 8 ГБ RAM, 80 ГБ NVMe — 1 289 ₽/мес при помесячной оплате. Portainer для Docker-контейнеров есть в маркетплейсе провайдера.
Промокод EASYLINK — скидка 60 % на первый месяц при оплате за 1 месяц, кроме тарифов Lite.
Реклама · ООО «Админвпс»
AdminVPS — сервер под CI/CD
Под раннеры и Docker Compose берите Start: 4 vCPU, 8 ГБ RAM, 80 ГБ NVMe — 1 289 ₽/мес при помесячной оплате. GitLab, Gitea и Portainer есть в каталоге приложений.
Промокод EASYLINK — скидка 60 % на первый месяц при оплате за 1 месяц, кроме тарифов Lite.
Частые вопросы
Какой VPS нужен для CI/CD с GitHub Actions?
Минимум: 2 vCPU, 4 ГБ RAM, 50 ГБ NVMe SSD с Docker. Для нескольких проектов: 4 vCPU, 8 ГБ RAM. Такая конфигурация стоит 1 289 ₽/мес при оплате за месяц у AdminVPS (тариф Start) и 1 800 ₽/мес при оплате за 12 месяцев у Timeweb Cloud (MSK 80).
Self-hosted runner или SSH-деплой: что лучше?
SSH-деплой проще в настройке и подходит для 1-2 проектов. Self-hosted runner быстрее (локальный Docker-кеш), без лимитов на минуты и подходит для команд.
Безопасно ли использовать self-hosted runner?
Для приватных репозиториев — да. Для публичных — нет: любой PR может выполнить код на вашем сервере. Используйте отдельного пользователя без sudo.
Как настроить zero-downtime деплой на VPS?
Без простоя — blue-green деплой: новая версия запускается рядом со старой, Nginx переключается на неё после проверки health, старая останавливается. Команда docker compose up -d --no-deps --build пересобирает один сервис, не трогая базу, но короткий простой у неё остаётся: Compose останавливает старый контейнер до запуска нового.
Сколько стоит GitHub Actions?
На бесплатном тарифе GitHub — 2 000 минут в месяц на раннерах GitHub для приватных репозиториев; в публичных стандартные раннеры бесплатны. Свой раннер (self-hosted) GitHub не тарифицирует — платите только за VPS.
Можно ли использовать GitLab CI вместо GitHub Actions?
Да, GitLab CI работает аналогично с GitLab Runner. Настройка .gitlab-ci.yml вместо .github/workflows/. Принципы Docker-деплоя на VPS идентичны.
VPS для CI/CD с Docker
Выбрать VPS для деплояНекоторые ссылки в этой статье являются партнёрскими. Мы можем получить комиссию при переходе и покупке. На стоимость для вас это не влияет. Подробнее