Многоэтапная сборка в Docker: практическое руководство для создания минимальных и безопасных образов | AdminWiki
Timeweb Cloud — сервера, Kubernetes, S3, Terraform. Лучшие цены IaaS.
Попробовать

Многоэтапная сборка в Docker: практическое руководство для создания минимальных и безопасных образов

25 мая 2026 10 мин. чтения
Содержание статьи

Многоэтапная сборка (multi-stage build) в Docker - это метод, который позволяет разделить процесс создания контейнера на несколько независимых стадий. Ключевая цель - получить финальный образ, содержащий только необходимые для запуска приложения файлы, без инструментов разработки, исходного кода и промежуточных артефактов. Для DevOps инженеров и системных администраторов это прямой путь к уменьшению размеров образов на 80-90%, сокращению поверхности для потенциальных атак и повышению скорости развертывания в CI/CD pipeline.

В отличие от стандартного Dockerfile, где все операции выполняются в одном контейнере, многоэтапная сборка использует несколько инструкций FROM. Каждая стадия начинается с нового базового образа, что позволяет на одной стадии компилировать код или устанавливать зависимости, а на другой - создавать чистый, минимальный runtime-образ, куда копируются только конечные артефакты. Этот подход радикально меняет логику создания контейнеров, превращая их из универсальных «строительных площадок» в компактные и безопасные исполняемые среды.

Проблема стандартных образов Docker: вес, риски и медленное развертывание

Типичный Dockerfile для приложения на Python с несколькими зависимостями может выглядеть так:

FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python", "app.py"]

Результат сборки такого файла - образ размером около 1.2 GB. В него попадает полная версия Python, все системные пакеты из базового дистрибутива, инструменты компиляции, исходный код проекта и все зависимости, включая те, которые нужны только для сборки. Этот избыточный вес создает три основные проблемы.

Как размер образов влияет на скорость CI/CD и стоимость инфраструктуры

Большие образы напрямую замедляют каждый этап жизненного цикла контейнера:

  • Время сборки и передачи: Загрузка образов в registry (например, AWS ECR) и их скачивание на production-серверы или в кластер Kubernetes требует больше времени и сетевого трафика. Образ 1.2 GB передается в 8 раз медленнее, чем образ 150 MB.
  • Влияние на pipeline: В CI/CD системах, таких как Jenkins или GitLab CI, этапы, зависящие от передачи образов (например, deployment), становятся узким местом, увеличивая общее время выполнения pipeline.
  • Затраты на хранилище: Хранение множества крупных образов и их тегов в приватном registry увеличивает расходы на облачные хранилища. Уменьшение размера на 80% сокращает эти затраты пропорционально.

С точки зрения безопасности, большой образ содержит больше компонентов - библиотек, инструментов, конфигурационных файлов. Каждый из них потенциально может иметь уязвимости (CVE). Наличие в финальном образе компилятора gcc или отладчика увеличивает риск, если контейнер будет скомпрометирован. Уменьшение размера означает сокращение «поверхности для атак».

Синтаксис многоэтапной сборки: FROM, AS, COPY - основа оптимизации

Многоэтапная сборка использует несколько ключевых инструкций Dockerfile:

  • FROM image:tag AS stage_name: Определяет начало новой стадии и присваивает ей имя для дальнейшего обращения.
  • COPY --from=stage_name /path /destination: Копирует файлы или директории из указанной стадии в текущую.
  • WORKDIR, RUN, ENV: Работают внутри своей стадии, их изменения не переносятся в следующую автоматически.

Логика разделения обычно включает две стадии: «builder» (строительная) и «runtime» (финальная). На стадии builder устанавливаются все зависимости, компилируется код, выполняются любые тяжелые операции. Стадия runtime начинается с минимального базового образа (например, python:3.11-slim или alpine), и в нее копируются только готовые артефакты из builder: бинарники, статические файлы, установленные пакеты без инструментов разработки. Для работы multi-stage builds требуется Docker версии 17.05 или выше.

Ключевые команды Dockerfile и их назначение в multi-stage

Команда Назначение в многоэтапной сборке
FROM ... AS name Создает новую стадию и задает ее имя для ссылок через COPY --from.
COPY --from=name Переносит файлы из указанной стадии в текущую. Это основной механизм передачи артефактов.
WORKDIR Устанавливает рабочую директорию внутри текущей стадии. Каждая стадия может иметь свой WORKDIR.
RUN Выполняет команды (установка пакетов, компиляция) только в контексте текущей стадии.
ARG Определяет переменные, доступные только во время сборки. Можно использовать для передачи параметров между стадиями.

Как избежать ошибок при переносе файлов между стадиями

Самая распространенная ошибка - попытка копировать всю директорию builder-стадии, включая исходный код, временные файлы и инструменты. Это приводит к увеличению финального образа и потенциальным проблемам безопасности. Правильный подход - копировать только конкретные конечные артефакты:

  • Для компилируемых языков (Go, Rust): только итоговый бинарный файл.
  • Для Python: только виртуальное окружение (/venv) или установленные пакеты из /usr/local.
  • Для Node.js: только директория node_modules (без devDependencies) и код приложения.

Ошибка в пути при использовании COPY --from приводит к сообщению Docker: «failed to compute cache key: failed to walk...». Чтобы избежать этого, убедитесь, что пути в builder-стадии и команде COPY соответствуют реальной структуре файлов. Используйте абсолютные пути относительно WORKDIR стадии builder.

Для глубокого понимания того, что именно хранится в ваших образах и как оптимизировать их слои, обратитесь к нашему руководству по анализу слоев Docker-образов с dive и docker inspect.

Пример для Python: от тяжелого Alpine с gcc до минимального образа

Рассмотрим практический пример для Python приложения с зависимостями, требующими компиляции C-кода (например, с использованием psycopg2 или numpy).

Dockerfile для Python приложения с зависимостями из requirements.txt

# Стадия builder: установка всех зависимостей и компиляция
FROM python:3.11-alpine AS builder
WORKDIR /build

# Копируем файл зависимостей и устанавливаем их с компилятором
COPY requirements.txt .
RUN apk add --no-cache gcc musl-dev postgresql-dev \
    && pip install --user --no-cache-dir -r requirements.txt

# Финальная стадия: минимальный runtime-образ
FROM python:3.11-slim AS runtime
WORKDIR /app

# Копируем только установленные пакеты из builder, исходный код не нужен
COPY --from=builder /root/.local /root/.local
# Копируем сам код приложения
COPY app.py .

# Устанавливаем PATH для найденных пакетов
ENV PATH=/root/.local/bin:$PATH

CMD ["python", "app.py"]

В builder-стадии используется python:3.11-alpine, который включает минимальный набор пакетов, но мы дополнительно добавляем gcc и другие dev-пакеты для компиляции C-библиотек. После установки зависимостей они сохраняются в директории /root/.local. Финальная стадия берет легкий образ python:3.11-slim и копирует только директорию с установленными пакетами и код приложения. Компиляторы и исходные зависимости из requirements.txt не попадают в финальный образ.

Метрики эффективности: сколько удалось сэкономить

  • Размер builder-стадии: ~1.1 GB (включает Alpine, gcc, все зависимости Python).
  • Размер финального образа (runtime): ~150 MB.
  • Уменьшение размера: более 85%.

Это сокращение не только экономит ресурсы, но и повышает безопасность: в финальном образе отсутствуют компилятор gcc, заголовочные файлы и другие инструменты разработки, которые могли быть использованы злоумышленником.

Пример для Go: использование статической компиляции для идеального минимума

Для Go-приложений многоэтапная сборка особенно эффективна благодаря возможности создания статически скомпилированных бинарников.

# Стадия builder: компиляция с необходимыми инструментами
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

# Финальная стадия: минимальный образ на основе scratch (пустой)
FROM scratch AS runtime
COPY --from=builder /build/app /app
CMD ["/app"]

В builder-стадии используется образ golang:1.21-alpine для компиляции. Флаг CGO_ENABLED=0 отключает использование C-библиотек, что позволяет создать полностью статический бинарник, не зависящий от системных библиотек в runtime. Финальная стадия использует базовый образ scratch, который абсолютно пуст. В него копируется только скомпилированный бинарник app.

  • Размер builder-стадии: ~350 MB (включает Go toolchain).
  • Размер финального образа: ~10-15 MB (только бинарник).
  • Уменьшение размера: более 95%.

Образ на основе scratch обладает максимальной безопасностью: в нем нет операционной системы, библиотек или любых других компонентов, кроме вашего приложения.

Пример для Node.js: отделение установки зависимостей от финального образа

Для Node.js приложений с большим количеством npm-пакетов многоэтапная сборка позволяет отделить установку зависимостей от финального runtime-образа.

# Стадия builder: установка всех зависимостей
FROM node:18-alpine AS builder
WORKDIR /build
COPY package*.json .
RUN npm ci --only=production

# Финальная стадия: только production зависимости и код
FROM node:18-alpine AS runtime
WORKDIR /app
COPY --from=builder /build/node_modules ./node_modules
COPY . .
CMD ["node", "server.js"]

В builder-стадии используется команда npm ci, которая обеспечивает точную и быструю установку зависимостей на основе package-lock.json. Флаг --only=production гарантирует, что devDependencies не будут установлены даже на этой стадии. Финальная стадия копирует только директорию node_modules и исходный код. Все инструменты npm и временные файлы сборки остаются в builder.

  • Размер builder-стадии: ~500 MB (включает Node.js, npm, все зависимости).
  • Размер финального образа: ~200 MB (только production dependencies и код).
  • Уменьшение размера: около 60%.

Этот подход также ускоряет повторные сборки, поскольку стадия builder может кэшироваться независимо, если не меняются package.json или package-lock.json.

Для создания безопасных и оптимизированных образов, соответствующих лучшим практикам 2026 года, ознакомьтесь с нашей подробной статьей о production-ready Dockerfile для Python, Node.js и Go.

Сравнение с другими методами оптимизации: когда multi-stage - лучший выбор

Многоэтапная сборка - не единственный метод уменьшения Docker-образов. Другие популярные подходы включают:

  • Distroless образы: Специализированные базовые образы от Google, содержащие только runtime-компоненты (например, для Java, Go). Они очень минимальны, но предлагают меньше контроля над содержимым, чем multi-stage, где вы определяете каждый перенесенный файл.
  • Инструменты динамической оптимизации (docker-slim): Эти инструменты анализируют работающий контейнер и удаляют неиспользуемые файлы. Они эффективны, но требуют дополнительного шага в pipeline и могут быть менее предсказуемыми, чем явное определение стадий в Dockerfile.

Многоэтапная сборка предоставляет максимальную гибкость и контроль на уровне Dockerfile. Она универсальна для любых языков и фреймворков. Этот метод рекомендуется для:

  • Компилируемых языков (Go, Rust, C++), где можно создать статический бинарник.
  • Приложений со сложными зависимостями, требующими инструментов разработки только на этапе сборки (Python с C-библиотеками, Node.js с native модулями).
  • Сценариев, где требуется максимальная безопасность и минимальный размер.

Distroless образы хорошо подходят для стандартных случаев, когда приложение работает на известном runtime (например, JVM). Инструменты типа docker-slim полезны для дальнейшей оптимизации уже существующих образов без изменения Dockerfile.

Интеграция в CI/CD pipeline: как автоматизировать и получить максимум скорости

Внедрение многоэтапной сборки в CI/CD pipeline не только уменьшает финальный образ, но также может оптимизировать процесс сборки благодаря кэшированию слоев.

В GitLab CI или GitHub Actions конфигурация этапа сборки остается стандартной: вы запускаете docker build. Однако важно правильно использовать тегирование:

  • Финальный образ следует тегировать с версией или меткой (например, myapp:latest, myapp:v1.2).
  • Builder-образы можно не тегировать и не публиковать в registry, они остаются только в локальном кэше Docker во время сборки.

Для мониторинга результатов внедрения добавьте в pipeline шаг, который измеряет и записывает размер финального образа после сборки. Это можно сделать с помощью команды docker images или скрипта, который отправляет метрики в мониторинговую систему. Сравнение размеров до и после оптимизации предоставит количественные данные об эффективности.

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

Резюме и итоговые рекомендации: что внедрить уже сегодня

Многоэтапная сборка в Docker - это проверенный метод для создания минимальных, безопасных и быстрых контейнеров. Его внедрение дает прямые преимущества для скорости развертывания, безопасности и стоимости инфраструктуры.

Язык / Сценарий Уменьшение размера Ключевые преимущества
Python (с C-библиотеками) 85-90% Удаление компиляторов, уменьшение поверхности для атак.
Go (статический бинарник) 95%+ Минимальный образ на scratch, максимальная безопасность.
Node.js (production dependencies) 60-70% Отделение dev-пакетов, улучшенное кэширование сборки.

Пошаговый план внедрения для вашего проекта:

  1. Анализ текущего Dockerfile: Определите, какие шаги требуют инструментов разработки (компиляторы, dev-пакеты) и какие файлы действительно нужны для запуска приложения.
  2. Разделение стадий: Перепишите Dockerfile, создав отдельную стадии builder для всех операций установки/компиляции. Используйте минимальный базовый образ для финальной стадии runtime.
  3. Тестирование: Соберите новый образ и убедитесь, что приложение работает корректно. Проверьте размер финального образа командой docker images.
  4. Интеграция в CI/CD: Обновите конфигурацию pipeline, убедитесь, что кэширование работает правильно, и добавьте мониторинг размеров образов.

Главное правило: финальный образ должен содержать только исполняемый код приложения и его минимальные runtime-зависимости. Все остальное должно оставаться на стадии builder и не попадать в production.

Для комплексного подхода к созданию и оптимизации Docker-образов от написания Dockerfile до production-среды рекомендуем ознакомиться с практическим руководством по созданию и оптимизации Docker образов, которое содержит готовые шаблоны и пошаговые инструкции.

Если вам требуется единый и удобный интерфейс для работы с более чем 200 моделями искусственного интеллекта, включая GPT, Gemini и Claude, без необходимости использования VPN и с оплатой в рублях, рассмотрите сервис AiTunnel. Он позволяет управлять бюджетами и ключами, интегрироваться через библиотеки OpenAI или использовать встроенный чат, что может быть полезно для автоматизации различных задач в DevOps.

Поделиться:
Сохранить гайд? В закладки браузера