CI/CD на пальцах: автодеплой за один вечер
Простое объяснение CI/CD и готовый пример автодеплоя через GitHub Actions.
Что такое CI/CD и зачем он вам
CI (Continuous Integration) — каждую правку собираем и тестируем автоматически.
CD (Continuous Delivery/Deployment) — после успеха выкатываем на стенд/прод без рук.
Плюсы: меньше ручных ошибок, быстрые релизы, прозрачность для команды и предсказуемые выкаты. 🧘♂️

Сервер: базовая подготовка (VPS)
# 1) Docker и compose plugin (Ubuntu)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# 2) Каталог приложения
sudo mkdir -p /opt/myapp && sudo chown -R $USER:$USER /opt/myapp
cd /opt/myapp
# 3) .env (на сервере, не коммитим)
cat > .env << 'ENV'
PORT=3000
ENV
# 4) docker-compose.yml
cat > docker-compose.yml << 'YAML'
services:
web:
image: registry.example.com/myapp:latest
env_file: .env
ports:
- "80:3000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 3s
retries: 5
YAML
# 5) Первый запуск (пока без CI)
docker compose pull && docker compose up -d
Dockerfile (минимальный пример для Node/PNPM)
# Dockerfile
FROM node:20-alpine AS deps
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm i --frozen-lockfile
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist dist
COPY package.json ./
RUN corepack enable && corepack prepare pnpm@latest --activate && pnpm i --prod --frozen-lockfile
EXPOSE 3000
CMD ["node", "dist/server.js"]
GitHub Actions: билд образа и деплой по SSH
Добавьте в репозиторий .github/workflows/deploy.yml
:
name: CI/CD Deploy
on:
push:
branches: [ "main" ]
permissions:
contents: read
packages: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build & push image
uses: docker/build-push-action@v6
with:
push: true
context: .
tags: ${{ secrets.REGISTRY_URL }}/${{ secrets.IMAGE_NAME }}:latest
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -e
cd /opt/myapp
docker compose pull
docker compose up -d
docker image prune -f
Секреты: REGISTRY_URL
, REGISTRY_USER
, REGISTRY_PASSWORD
, IMAGE_NAME
, SERVER_HOST
, SERVER_USER
, SERVER_SSH_KEY
.
GitLab CI: альтернатива
# .gitlab-ci.yml
stages: [build, deploy]
variables:
IMAGE: $CI_REGISTRY_IMAGE:latest
build:
stage: build
image: docker:27.0
services: [ "docker:27.0-dind" ]
script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- docker build -t $IMAGE .
- docker push $IMAGE
artifacts:
expire_in: 1 week
when: on_success
paths: []
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
script:
- ssh -o StrictHostKeyChecking=no $DEPLOY_USER@$DEPLOY_HOST "
set -e
cd /opt/myapp &&
docker compose pull &&
docker compose up -d &&
docker image prune -f
"
only:
- main
Ноль простоя и откаты
Zero‑downtime:
restart: unless-stopped
+healthcheck
+up -d
— новый контейнер стартует, старый гаснет.Теги релизов: помимо
:latest
пушьте:v1.4.2
— проще откатиться.Rollback: переключите тег в
docker-compose.yml
и повторитеpull
/up -d
.Синяя/зелёная схема (упрощённо): держите два сервиса
web_blue
иweb_green
, балансируйте трафик через Nginx.
Минимальные проверки в пайплайне
# пример шага с тестами в GitHub Actions
- name: Install deps & test
run: |
npm ci
npm run test -- --ci
Порог для «вечернего» CI: прогнать юнит‑тесты, собрать билд, пройти линтер (ESLint/flake8), проверить типы (tsc/mypy).
Мониторинг и логирование
Добавьте
/health
и/metrics
(Prometheus формат) в сервис.Сбор логов:
docker logs
→ Loki/ELK; алерты по статус‑коду/латентности.В CI — артефакты с отчётами тестов и линтера, чтобы видеть регрессии.
Секреты и безопасность
Секреты только в Secrets/Variables CI;
.env
— на сервере.Отключите парольный SSH‑вход, оставьте вход по ключу, ограничьте порты.
Не храните приватные ключи в репозитории, даже в зашифрованном виде.
🧯 Типовые проблемы и быстрые решения
Симптом | Причина | Фикс |
---|---|---|
CI падает на сборке | Мало RAM/таймаут | Кэш слоёв, более лёгкая база образа, увеличить timeout |
Приложение не поднимается | Порт/ENV/миграции | Проверьте |
Деплой медленный | Тяжёлый образ | Multi‑stage, alpine, .dockerignore, кеширование зависимостей |
«Работает у меня» | Разные версии Node/Python | Фиксируйте версии в Docker, используйте lock‑файлы |
Диск забился | Старые образы/контейнеры |
|
В приложении «Кодик — обучение программированию» — короткие уроки и мини‑проекты. У нас интересно!
А ещё у нас есть активный telegram-канал, где мы обсуждаем крутые идеи, делимся опытом и вместе разбираем задачи — учиться становится не только полезно, но и весело.
Итог
CI/CD — это не «большая магия», а набор простых шагов. Сборка образа, пуш в реестр, перезапуск сервиса на VPS — и у вас быстрые, повторяемые релизы без ручной рутины. Начните сегодня, а завтра команда забудет, как выглядел «ручной деплой».
На чём вы собираетесь крутить автодеплой — GitHub Actions или GitLab CI?