CI/CD на пальцах: автодеплой за один вечер

Простое объяснение CI/CD и готовый пример автодеплоя через GitHub Actions.

РазработкаОсновы

6 мин

Что такое 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/миграции

Проверьте .env, docker logs, healthcheck, прогоните миграции

Деплой медленный

Тяжёлый образ

Multi‑stage, alpine, .dockerignore, кеширование зависимостей

«Работает у меня»

Разные версии Node/Python

Фиксируйте версии в Docker, используйте lock‑файлы

Диск забился

Старые образы/контейнеры

docker system prune -f, ротация логов

В приложении «Кодик — обучение программированию» — короткие уроки и мини‑проекты. У нас интересно!

А ещё у нас есть активный telegram-канал, где мы обсуждаем крутые идеи, делимся опытом и вместе разбираем задачи — учиться становится не только полезно, но и весело.

Итог

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

На чём вы собираетесь крутить автодеплой — GitHub Actions или GitLab CI?

Комментарии