Ручной деплой — это риск. Каждый раз, когда разработчик вручную заливает код на сервер, есть шанс что-то сломать: забыть переменную окружения, перепутать ветку, пропустить миграцию базы данных. CI/CD убирает человека из этого процесса. Разберём, как выстроить надёжный пайплайн в GitLab от первого коммита до production.
Структура .gitlab-ci.yml
Всё начинается с файла .gitlab-ci.yml в корне репозитория. Минимальная структура:
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
default:
image: node:20-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
Ключевые концепции:
| Понятие | Описание |
|---|---|
| stages | Упорядоченные группы jobs. Jobs одной стадии выполняются параллельно |
| jobs | Атомарные задачи: тест, сборка, деплой |
| runners | Агенты, которые выполняют jobs |
| artifacts | Файлы, передаваемые между стадиями |
| cache | Кешированные зависимости между запусками |
| environments | Именованные окружения (staging, production) |
Стадии и jobs
Типичный пайплайн для веб-приложения:
stages:
- lint
- test
- build
- deploy:staging
- deploy:production
# --- LINT ---
lint:
stage: lint
script:
- npm ci
- npm run lint
- npm run type-check
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# --- ТЕСТЫ ---
unit_tests:
stage: test
script:
- npm ci
- npm run test:unit -- --coverage
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
expire_in: 1 week
e2e_tests:
stage: test
image: mcr.microsoft.com/playwright:v1.45.0-jammy
script:
- npm ci
- npx playwright install --with-deps chromium
- npm run test:e2e
artifacts:
when: on_failure
paths:
- playwright-report/
expire_in: 3 days
# --- СБОРКА ---
build_image:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- |
docker build \
--cache-from $CI_REGISTRY_IMAGE:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
-t $CI_REGISTRY_IMAGE:latest \
.
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- tags
Docker Runners
По умолчанию GitLab.com предоставляет shared runners. Для приватных серверов нужен self-hosted runner.
Установка runner на VPS
# Установка
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install gitlab-runner
# Регистрация
sudo gitlab-runner register \
--url https://gitlab.com \
--registration-token YOUR_TOKEN \
--executor docker \
--docker-image "docker:24" \
--docker-privileged \
--description "production-runner" \
--tag-list "production,docker"
Конфигурация runner (/etc/gitlab-runner/config.toml)
[[runners]]
name = "production-runner"
url = "https://gitlab.com"
executor = "docker"
[runners.docker]
tls_verify = false
image = "docker:24"
privileged = true
disable_entrypoint_overwrite = false
oom_kill_disable = false
disable_cache = false
volumes = ["/certs/client", "/cache"]
shm_size = 0
[runners.cache]
Type = "s3"
Shared = true
[runners.cache.s3]
ServerAddress = "minio.example.com"
BucketName = "gitlab-runner-cache"
BucketLocation = "us-east-1"
Артефакты и передача данных между стадиями
Артефакты — это способ передать результаты одной стадии в следующую:
build_frontend:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour # хранить 1 час (достаточно для деплоя)
deploy_staging:
stage: deploy:staging
dependencies:
- build_frontend # явно указываем источник артефактов
script:
- ls dist/ # файлы доступны здесь
- rsync -avz dist/ user@staging.example.com:/var/www/app/
Артефакты для отчётов (покрытие, тесты) не нужно передавать явно — GitLab подхватывает их автоматически:
unit_tests:
artifacts:
reports:
junit: junit.xml # результаты тестов в MR
coverage_report:
coverage_format: cobertura
path: coverage.xml
paths:
- coverage/html/ # HTML-отчёт о покрытии
expire_in: 30 days
Управление секретами
Никогда не храните секреты в .gitlab-ci.yml. Используйте CI/CD Variables (Settings → CI/CD → Variables):
deploy_production:
script:
# $DATABASE_URL, $JWT_SECRET и т.д. — CI/CD Variables
- |
docker run -d \
--name app \
-e DATABASE_URL=$DATABASE_URL \
-e JWT_SECRET=$JWT_SECRET \
-e REDIS_URL=$REDIS_URL \
-p 3000:3000 \
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
Для production используйте Protected variables — они доступны только в protected ветках (main, production):
# Переменные с пометкой Protected не будут доступны в feature-ветках
# Устанавливается в GitLab UI: Settings → CI/CD → Variables → Protected ✅
Для чувствительных данных рекомендуем HashiCorp Vault:
deploy_production:
secrets:
DATABASE_PASSWORD:
vault: production/db/password@secret
file: false
Кеширование зависимостей
Правильный кеш ускоряет пайплайн в 3-5 раз:
default:
cache:
# Разный кеш для разных веток
key:
files:
- package-lock.json # инвалидация при изменении lock-файла
prefix: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
policy: pull-push
# Отдельный job для обновления кеша (только в main)
update_cache:
stage: .pre
script:
- npm ci --cache .npm --prefer-offline
cache:
key:
files:
- package-lock.json
prefix: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/
policy: push # только записывает, не читает
only:
changes:
- package-lock.json
Окружения и деплой
Staging автоматически, Production вручную
deploy_staging:
stage: deploy:staging
environment:
name: staging
url: https://staging.example.com
script:
- ./scripts/deploy.sh staging $CI_COMMIT_SHA
only:
- main
deploy_production:
stage: deploy:production
environment:
name: production
url: https://example.com
script:
- ./scripts/deploy.sh production $CI_COMMIT_SHA
when: manual # нажать кнопку в GitLab UI
only:
- main
allow_failure: false
Динамические окружения для review apps
deploy_review:
stage: deploy:staging
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.review.example.com
on_stop: stop_review # автоудаление при закрытии MR
script:
- helm upgrade --install review-$CI_COMMIT_REF_SLUG ./helm \
--set image.tag=$CI_COMMIT_SHA \
--set ingress.host=$CI_COMMIT_REF_SLUG.review.example.com
only:
- merge_requests
stop_review:
stage: deploy:staging
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
script:
- helm uninstall review-$CI_COMMIT_REF_SLUG
when: manual
only:
- merge_requests
Стратегии деплоя
Blue-Green Deployment
Два идентичных окружения: Blue (текущий production) и Green (новая версия). Переключение моментальное:
#!/bin/bash
# scripts/blue-green-deploy.sh
CURRENT=$(docker inspect --format='{{.Name}}' $(docker ps -q --filter name=app-blue) 2>/dev/null | grep -c blue)
if [ "$CURRENT" -gt 0 ]; then
# Текущий — blue, деплоим в green
NEW_COLOR=green
OLD_COLOR=blue
else
NEW_COLOR=blue
OLD_COLOR=green
fi
echo "Deploying to $NEW_COLOR..."
# Запускаем новую версию
docker run -d \
--name app-$NEW_COLOR \
--health-cmd="curl -f http://localhost:3000/health || exit 1" \
--health-interval=10s \
--health-retries=3 \
$IMAGE_TAG
# Ждём готовности
timeout 60 bash -c "until docker inspect --format='{{.State.Health.Status}}' app-$NEW_COLOR | grep -q healthy; do sleep 2; done"
# Переключаем Nginx
sed -i "s/app-$OLD_COLOR/app-$NEW_COLOR/" /etc/nginx/conf.d/app.conf
nginx -s reload
echo "Traffic switched to $NEW_COLOR"
# Останавливаем старую версию
sleep 30 # grace period для завершения текущих запросов
docker stop app-$OLD_COLOR && docker rm app-$OLD_COLOR
Rolling Update (через Docker Swarm или Kubernetes)
# docker-compose.production.yml
services:
app:
image: ${IMAGE_TAG}
deploy:
replicas: 4
update_config:
parallelism: 1 # обновляем по 1 контейнеру
delay: 30s # ждём 30 секунд между заменами
failure_action: rollback
monitor: 60s
max_failure_ratio: 0.25
rollback_config:
parallelism: 0 # откатываем все сразу
failure_action: pause
# В GitLab CI
deploy_production:
script:
- docker stack deploy -c docker-compose.production.yml myapp --with-registry-auth
environment:
name: production
Полный пайплайн в итоге
# Итоговая структура
stages: [lint, test, build, deploy:staging, deploy:production]
# Lint: параллельно eslint + type-check
# Test: unit (с coverage) + e2e (с артефактами при падении)
# Build: Docker image → GitLab Registry
# Deploy staging: автоматически при пуше в main
# Deploy production: вручную через кнопку в UI
Время пайплайна с кешированием: ~4-6 минут. Без кеша — 12-15 минут.
О том, как оптимизировать Docker-образы для ускорения сборки, читайте в статье Оптимизация Dockerfile. Для Next.js-приложений также посмотрите Next.js разработка: App Router — там есть пример многоэтапного Dockerfile.
Итог
Хороший CI/CD пайплайн — это не разовая настройка, а живой инструмент команды. Начните с простого: lint → тесты → деплой на staging. Добавляйте шаги постепенно: e2e тесты, review apps, blue-green. Главное — автоматизировать то, что сейчас делается руками и регулярно ошибается.

