Главная / База знаний / CI/CD с Docker: сборка и деплой образов в пайплайне
инструкция · 12 ноября 2026

CI/CD с Docker: сборка и деплой образов в пайплайне

Как собрать Docker-образ в CI, правильно его тегировать, опубликовать в реестр и развернуть на VPS: практический пайплайн на GitHub Actions или GitLab CI, multi-stage сборка и откат на предыдущую версию.

Зачем собирать Docker-образ именно в CI, а не вручную

CI/CD (Continuous Integration / Continuous Deployment) для контейнеризованного приложения на практике означает автоматизацию всего пути от изменения в коде до работающей новой версии на сервере: система непрерывной интеграции автоматически собирает Docker-образ из актуального кода при каждом подходящем событии в репозитории, публикует его в реестр образов и, при необходимости, разворачивает на целевом сервере — без единого ручного шага со стороны разработчика после отправки изменений в репозиторий.

Ручная сборка и публикация образа с локальной машины разработчика создаёт целый ряд практических проблем, которые сборка в CI устраняет системно: разница в окружении сборки между машинами разных разработчиков (разная версия Docker, разные локально закешированные слои образов) способна привести к тому, что "у меня собирается" не гарантирует такой же результат сборки у коллеги или на сервере; человеческий фактор при ручной публикации (забытый тег, публикация не той ветки, пропущенный шаг тестирования перед публикацией) отсутствует при полностью автоматизированном процессе; и, что важно для аудита, каждая сборка в CI оставляет чёткий, воспроизводимый след — какой именно коммит, какой запуск пайплайна и в какое время привёл к появлению конкретного образа.

Эта статья разбирает практический пайплайн CI/CD для Docker-приложения: сборку образа в GitHub Actions или GitLab CI, разумную стратегию тегирования, публикацию в реестр, multi-stage сборку для уменьшения размера образа, и развёртывание новой версии на целевом VPS с возможностью быстрого отката на предыдущую версию.

Multi-stage сборка: компактный образ без лишних инструментов

Многие приложения требуют для своей сборки набора инструментов, совершенно не нужных во время реальной работы уже собранного приложения — компиляторы, менеджеры пакетов для разработки, инструменты сборки фронтенда вроде webpack, полный набор dev-зависимостей. Включение всех этих инструментов в финальный образ раздувает его размер без какой-либо пользы для рабочего окружения, только замедляя загрузку и увеличивая поверхность потенциальных уязвимостей.

Первая стадия использует полный образ Node.js для сборки, финальная — только облегчённый alpine-образ с готовыми артефактами
# --- стадия сборки ---
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# --- финальная стадия ---
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]

Директива FROM node:20 AS build именует первую стадию сборки, а COPY --from=build в финальной стадии копирует из неё только конкретные, уже собранные результаты (в примере — каталог dist с готовым приложением), не перенося в финальный образ ни исходный код до сборки, ни devDependencies, ни сами инструменты сборки вроде компилятора TypeScript или сборщика фронтенда. Финальный образ строится на базе node:20-alpine — заметно более компактном варианте базового образа, содержащем минимальный набор системных пакетов, достаточный для запуска уже собранного приложения.

Практический эффект multi-stage сборки хорошо заметен именно на сравнении размеров: образ, включающий полный инструментарий сборки, компилятор и все зависимости для разработки, легко может быть в несколько раз больше эквивалентного финального образа, содержащего только результат сборки и минимальные рабочие зависимости, — меньший размер образа означает не только более быструю передачу по сети при публикации и развёртывании, но и сокращённую поверхность для потенциальных уязвимостей за счёт отсутствия ненужных во время работы пакетов.

Стратегия тегирования: git-хеш вместо голого latest

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

Публикация образа сразу под двумя тегами: неизменяемым хешем коммита и изменяемым latest
# в пайплайне CI
IMAGE_TAG=$(git rev-parse --short HEAD)
docker build -t registry.example.com/myapp:$IMAGE_TAG .
docker build -t registry.example.com/myapp:latest .
docker push registry.example.com/myapp:$IMAGE_TAG
docker push registry.example.com/myapp:latest

Тег latest удобен для быстрого получения самой свежей версии образа вручную, но не подходит как единственный способ отслеживания версий в production, поскольку он по определению всегда переопределяется следующей публикацией и не позволяет однозначно понять постфактум, какая версия кода была развёрнута в конкретный момент времени в прошлом. Тег на основе короткого git-хеша коммита (или, ещё лучше, тега git-релиза для более редких, осознанных публикаций) неизменяем — он всегда указывает на одну и ту же, зафиксированную версию кода, что и делает возможным точный, предсказуемый откат на любую конкретную предыдущую версию.

Практическая рекомендация — публиковать образ сразу под двумя тегами одновременно: неизменяемым (git-хеш или номер версии) для точной идентификации и возможности отката, и latest или окружение-специфичным тегом вроде staging или production для удобства ручного получения актуальной версии там, где точная версия не так критична, например при локальной отладке разработчиком.

Пример пайплайна в GitHub Actions

GitHub Actions описывает пайплайн через YAML-файл в каталоге .github/workflows репозитория, где определяются события, запускающие пайплайн, и последовательность шагов (jobs и steps) для его выполнения.

Сборка и публикация образа при каждом пуше в основную ветку main
name: Build and deploy
on:
  push:
    branches: [ main ]

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Log in to registry
        run: echo "${{ secrets.REGISTRY_PASSWORD }}" | \
             docker login registry.example.com -u deploy --password-stdin
      - name: Build and push
        run: |
          TAG=$(git rev-parse --short HEAD)
          docker build -t registry.example.com/myapp:$TAG .
          docker build -t registry.example.com/myapp:latest .
          docker push registry.example.com/myapp:$TAG
          docker push registry.example.com/myapp:latest

Секция on: push: branches: [ main ] ограничивает запуск пайплайна только пушами непосредственно в основную ветку, а не любым коммитом в любой ветке репозитория, — это разумное ограничение для полного цикла сборки и публикации образа, тогда как для промежуточных веток разработки или pull request обычно достаточно отдельного, более лёгкого пайплайна с запуском тестов без полной публикации нового образа. Секреты вроде пароля от реестра образов хранятся в защищённом хранилище GitHub Actions Secrets и подставляются в пайплайн через синтаксис ${{ secrets.ИМЯ }}, никогда не попадая в текст самого файла конфигурации, видимый в репозитории.

Для GitLab CI логика аналогична, но описывается в файле .gitlab-ci.yml с собственным синтаксисом стадий (stages) и заданий (jobs), а секреты хранятся в разделе CI/CD Variables настроек проекта — сам подход к разделению шагов сборки, тестирования и публикации остаётся принципиально тем же независимо от конкретной системы CI.

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

Реестр образов (registry) — хранилище, откуда собранные образы получают серверы для развёртывания, и выбор конкретного реестра зависит от требований к приватности и уже используемой инфраструктуры проекта. GitHub Container Registry (ghcr.io) и GitLab Container Registry удобны тем, что тесно интегрированы с самой платформой хранения кода и системой контроля доступа к репозиторию, не требуя отдельной регистрации в стороннем сервисе.

Публикация образа в GitHub Container Registry с использованием токена доступа
# GitHub Container Registry
echo "$GITHUB_TOKEN" | docker login ghcr.io -u USERNAME --password-stdin
docker tag myapp:latest ghcr.io/username/myapp:latest
docker push ghcr.io/username/myapp:latest

Docker Hub остаётся наиболее известным публичным реестром, но его бесплатный уровень исторически применял ограничения на число операций получения образа (image pull) в единицу времени для анонимных и даже авторизованных пользователей — это стоит учитывать при частых автоматических развёртываниях с сервера, если сервер обращается к реестру без авторизации или с ограниченным по частоте использования аккаунтом. Для проприетарного кода приватный реестр обязателен — публичный реестр по умолчанию делает опубликованные образы видимыми всем, если явно не настроена приватность конкретного репозитория образов.

Каждый образ в реестре стоит снабжать разумной политикой хранения (retention policy) — без неё число накопленных тегов образов, особенно при тегировании по git-хешу на каждый коммит, будет расти неограниченно, постепенно занимая всё больше места в хранилище реестра; большинство реестров поддерживают автоматическую очистку образов старше определённого срока или сверх определённого числа последних версий.

Развёртывание новой версии на целевом VPS

После публикации нового образа в реестр целевой сервер должен получить его и перезапустить контейнер уже с новой версией — на практике это чаще всего реализуется через SSH-подключение из пайплайна CI к серверу и выполнение там команд получения нового образа и перезапуска сервиса.

Подключение к серверу по SSH, получение нового образа сервиса web и его перезапуск без остановки остальных сервисов
# шаг деплоя в пайплайне, после успешной публикации образа
- name: Deploy to VPS
  uses: appleboy/ssh-action@v1
  with:
    host: ${{ secrets.VPS_HOST }}
    username: deploy
    key: ${{ secrets.VPS_SSH_KEY }}
    script: |
      cd /opt/myapp
      docker compose pull web
      docker compose up -d web
      docker image prune -f

Команда docker compose pull web получает только образ конкретного сервиса web, указанный в docker-compose.yml (обычно ссылающийся на тег latest или на конкретный тег окружения, обновляемый пайплайном), а docker compose up -d web пересоздаёт только контейнер этого сервиса, оставляя остальные, не изменившиеся сервисы того же стека нетронутыми и продолжающими работать без прерывания. Команда docker image prune -f удаляет более не используемые старые образы, накопившиеся на сервере после серии предыдущих обновлений, освобождая место на диске.

SSH-ключ для доступа к серверу деплоя должен быть создан специально для этой цели, с правами, ограниченными минимально необходимым набором действий на сервере (в идеале — отдельный пользователь deploy без полных прав root, имеющий доступ только к нужному для деплоя каталогу и группе docker), а не переиспользовать личный ключ администратора, дающий полный доступ ко всему серверу.

Откат на предыдущую версию

Возможность быстрого отката — прямое следствие правильно выбранной стратегии тегирования: если каждый опубликованный образ имеет собственный неизменяемый тег на основе git-хеша, откат сводится к повторному развёртыванию контейнера, указывающего на тег предыдущего известного рабочего коммита, вместо голого latest.

Откат на конкретную предыдущую версию образа по её неизменяемому тегу
# на сервере, при обнаруженной проблеме с последней версией
docker pull registry.example.com/myapp:a1b2c3d
docker tag registry.example.com/myapp:a1b2c3d registry.example.com/myapp:latest
docker compose up -d web

Такой откат занимает время, сопоставимое с обычным развёртыванием новой версии — не требуется пересобирать код заново, откатывать коммиты в git или разбираться с отдельной веткой восстановления, поскольку образ предыдущей рабочей версии уже собран и опубликован в реестре, и остаётся только заново указать на него в конфигурации и перезапустить контейнер. Именно эта скорость и предсказуемость отката — одна из главных практических причин, почему тегирование неизменяемым идентификатором коммита стоит того небольшого дополнительного усилия по сравнению с использованием одного лишь latest.

Для более автоматизированного отката без ручного подключения по SSH можно предусмотреть отдельный, отключаемый пайплайн (например, ручной запуск в GitHub Actions через workflow_dispatch с параметром — конкретным тегом для отката), который выполняет ту же последовательность команд удалённо, снижая зависимость от того, что администратор помнит точный синтаксис команд отката именно в момент инцидента, когда особенно легко ошибиться из-за спешки.

Типичные ошибки в пайплайне CI/CD для Docker

Публикация образов исключительно под тегом latest без параллельного неизменяемого тега — лишает возможности точно определить постфактум, какая версия кода была развёрнута в конкретный момент, и делает быстрый точный откат на конкретную предыдущую версию практически невозможным.

Хранение паролей от реестра образов или SSH-ключей для деплоя прямо в тексте файла конфигурации пайплайна вместо защищённого хранилища секретов конкретной CI-системы — прямая утечка учётных данных для всех, у кого есть доступ на чтение к репозиторию, включая историю его коммитов, где старое значение может остаться навсегда, даже если позже удалено из актуальной версии файла.

Отсутствие multi-stage сборки для приложений, требующих отдельного этапа компиляции или сборки фронтенда, — раздувает финальный образ инструментами и зависимостями, ненужными во время реальной работы приложения, замедляя развёртывание и увеличивая поверхность потенциальных уязвимостей без какой-либо практической пользы.

Развёртывание новой версии сразу на единственный работающий сервер без какой-либо проверки собранного образа перед этим (хотя бы базового запуска в CI и проверки, что приложение внутри контейнера успешно стартует) — переносит обнаружение поломки с этапа CI на этап реального влияния на пользователей production-окружения.

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

Итог: предсказуемый путь от коммита до работающего сервера

Полноценный CI/CD пайплайн для Docker-приложения — это не разовая настройка ради самого факта автоматизации, а системный способ устранить главные источники ошибок ручного процесса: несогласованность окружений сборки, забытые шаги публикации и отсутствие чёткого, воспроизводимого следа о том, какая именно версия кода сейчас реально работает на сервере. Multi-stage сборка держит образы компактными, тегирование по git-хешу делает версии однозначно идентифицируемыми, а автоматизированный деплой через SSH или более специализированные инструменты убирает человеческий фактор из повторяющегося процесса.

Настроенный таким образом пайплайн окупается не столько в спокойные периоды, сколько именно в момент, когда новая версия сломала что-то в production и нужен быстрый, предсказуемый откат без паники и ручного разбора, что именно было развёрнуто и как это отменить. Если нужна помощь с настройкой CI/CD пайплайна под конкретный стек технологий проекта или выбором подходящего реестра образов, опишите задачу в заявке внизу страницы.

почему мы

Почему стоит выбрать нас

Быстрый запуск

Подберём конфигурацию и развернём проект в день обращения.

Надёжность

NVMe-диски, защита от DDoS и резервные копии по расписанию.

Поддержка 24/7

Дежурные инженеры на связи в любое время суток.

Настройка под ключ

Установим CMS, настроим веб-сервер, SSL и почту за вас.

Перенос без простоя

Аккуратно перенесём сайт и базу данных на новый сервер.

Прозрачные цены

Актуальные тарифы и честный подбор без навязывания.

частые вопросы

Вопросы и ответы

Почему нельзя просто всегда использовать тег latest для образа?

Тег latest не гарантирует какую-либо конкретную версию — он просто по соглашению указывает на последний опубликованный образ и постоянно переопределяется при каждой новой публикации, поэтому по одному только тегу latest невозможно понять, какая именно версия кода сейчас реально развёрнута на сервере, и невозможно откатиться на конкретную предыдущую версию, если новая публикация уже перезаписала этот тег.

Что такое multi-stage сборка и зачем она нужна?

Это разбиение Dockerfile на несколько последовательных стадий, где ранние стадии могут включать полный набор инструментов сборки (компиляторы, менеджеры пакетов для разработки), а финальная стадия копирует из предыдущих только собранные артефакты в минимальный базовый образ — это заметно уменьшает размер итогового образа за счёт исключения из него инструментов, нужных только на этапе сборки, но не во время работы приложения.

Нужно ли пересобирать Docker-образ на каждый пуш в любую ветку репозитория?

Не обязательно и часто не рационально — типичная практика ограничивает автоматическую сборку и публикацию образа определёнными событиями: пуш в основную ветку, создание тега релиза или пуш в ветку, из которой открыт pull request, тогда как промежуточные коммиты в рабочей ветке разработки могут ограничиваться только запуском тестов без полной сборки и публикации образа.

Как быстро откатиться на предыдущую версию, если новый деплой сломал прод?

Если образы тегируются неизменяемым идентификатором вроде git-хеша коммита, откат сводится к повторному развёртыванию контейнера из образа с тегом предыдущего известного рабочего коммита, что занимает время, сопоставимое с обычным деплоем, а не требует пересборки кода заново или отдельной ветки восстановления.

Безопасно ли хранить пароль от Docker-реестра прямо в файле пайплайна CI?

Нет, учётные данные для публикации образа в реестр должны храниться в защищённом хранилище секретов конкретной CI-системы (GitHub Actions Secrets, GitLab CI/CD Variables), а не в самом тексте файла конфигурации пайплайна, который обычно хранится в том же репозитории и виден всем, у кого есть доступ на чтение к репозиторию.

Чем публичный реестр вроде Docker Hub отличается от приватного вроде GitHub Container Registry?

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

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

Нет, docker compose позволяет обновить и пересоздать конкретный отдельный сервис командой docker compose up -d конкретный_сервис без остановки и пересоздания остальных, не изменившихся сервисов того же стека, что сокращает время простоя и не затрагивает части приложения, не связанные с обновлением.

Можно ли тестировать сам Dockerfile перед его публикацией в CI?

Да, собранный образ можно и стоит запускать во временном контейнере прямо в CI-пайплайне для базовой проверки (например, что приложение внутри контейнера успешно стартует и отвечает на проверку здоровья) до того, как этот образ будет опубликован в реестр и станет доступен для развёртывания на реальном сервере.

Как оплачивать услуги?

Оплата помесячная, для облачных серверов доступна почасовая тарификация. Принимаются карты, банковские переводы и электронные кошельки.

Есть ли круглосуточная поддержка?

Да, дежурные инженеры на связи 24/7.

Выдаёте ли закрывающие документы?

Да, предоставим необходимые документы для оплаты.

Как оставить заявку?

Заполните форму на странице или нажмите «Заказать» — мы свяжемся и поможем с подбором.

заявка

Оставьте заявку

Ответим и поможем с подбором и настройкой.