Понимание многоэтапной сборки Docker образов

Понимание многоэтапной сборки Docker образов
Photo by Ian Taylor / Unsplash

Введение

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

Многоэтапные сборки Docker предлагают решение этих проблем. Они позволяют создавать оптимизированные Docker-образы, используя несколько этапов в рамках одного Dockerfile. Каждый этап представляет собой отдельную среду сборки, что позволяет отделить зависимости сборки от зависимостей среды выполнения. Такой подход приводит к созданию меньших по размеру, более безопасных и легких в обслуживании конечных образов.

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

Понимание одноэтапных сборок Docker

Прежде чем погрузиться в многоэтапные сборки, давайте рассмотрим традиционные одноэтапные сборки Docker и их характеристики. Традиционный одноэтапный Dockerfile обычно состоит из инструкций, определяющих процесс сборки Docker-образа. Вот пример:

FROM golang:1.22

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o main .

EXPOSE 8080

CMD ["./main"]

Этот одноэтапный Dockerfile начинается с официального базового образа Go 1.22, устанавливает рабочий каталог, копирует необходимые файлы, загружает зависимости, собирает Go-приложение и открывает нужный порт. Полученный образ объединяет процесс сборки и среду выполнения в одном этапе, что приводит к большему размеру образа, включающему компилятор Go и все зависимости сборки.

У одноэтапных сборок Docker есть некоторые преимущества:

  • Простота: Одноэтапные сборки легко понять, особенно для более простых приложений.
  • Привычность: Многие разработчики привыкли писать одноэтапные Dockerfile, что делает их распространенным подходом.

Однако одноэтапные сборки в Docker также имеют ряд ограничений и могут привести к различным проблемам:

  • Большой размер образа: Одноэтапные сборки часто приводят к большим размерам образов, так как они включают в себя как зависимости сборки, так и зависимости среды выполнения в конечном образе. Это может привести к увеличению требований к хранилищу и замедлению времени передачи образа.
  • Более длительное время сборки: По мере роста образа из-за включения зависимостей сборки процесс сборки становится медленнее, особенно если зависимости сборки большие или сложные. Это может повлиять на продуктивность разработки и замедлить общий цикл разработки.
  • Проблемы безопасности: Включение инструментов сборки и ненужных зависимостей в конечный образ может увеличить поверхность атаки и создать потенциальные уязвимости безопасности. Образы для выполнения в идеале должны содержать только необходимые компоненты для запуска приложения, минимизируя риск проблем с безопасностью.
  • Обслуживание Dockerfile: По мере развития приложений поддержка одноэтапного Dockerfile может стать сложной и подверженной ошибкам, особенно при работе с несколькими этапами сборки и зависимостями. Сохранение Dockerfile чистым, читаемым и поддерживаемым со временем становится сложной задачей.
  • Неэффективное кэширование: Одноэтапные сборки могут не эффективно использовать механизмы кэширования, предоставляемые Docker. Если зависимости сборки или ранние этапы процесса сборки изменяются, необходимо повторно выполнить всю сборку, что приводит к избыточным сборкам и более медленным циклам разработки.

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

Многоэтапные сборки Docker

Введение многоэтапных сборок Docker

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

Многоэтапный Dockerfile состоит из нескольких инструкций FROM, каждая из которых представляет отдельный этап со своим базовым образом и инструкциями. Вот пример:

# Этап сборки
FROM golang:1.22 AS build
WORKDIR /app
COPY . .
RUN go build -o main .

# Этап выполнения
FROM alpine:3.20
WORKDIR /app
COPY --from=build /app/main .
CMD ["./main"]

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

  1. Меньший размер образов: Включая только необходимые компоненты времени выполнения, многоэтапные сборки создают значительно меньшие образы по сравнению с одноэтапными сборками. Меньшие образы приводят к более быстрой передаче образов, уменьшению требований к хранилищу и более быстрому запуску контейнеров.
  2. Улучшенная безопасность: Исключение инструментов сборки, компиляторов и зависимостей разработки из конечного образа уменьшает поверхность атаки и минимизирует риск уязвимостей безопасности.
  3. Лучшая поддерживаемость: Разделение этапов сборки и выполнения делает Dockerfile более модульным и легким в обслуживании. Вы можете обновлять зависимости сборки, не влияя на среду выполнения, и наоборот.
  4. Более быстрые сборки: Многоэтапные сборки могут более эффективно использовать кэширование. Если зависимости сборки или код приложения не изменились, последующие сборки могут повторно использовать кэшированные слои, что приводит к более быстрому времени сборки.
  5. Параллелизация: Многоэтапные сборки позволяют распараллеливать процесс сборки. Разные этапы могут быть собраны одновременно, что позволяет ускорить общее время сборки. Это особенно полезно для сложных приложений с несколькими компонентами или зависимостями.
  6. Гибкость: Многоэтапные сборки предлагают гибкость в выборе различных базовых образов для каждого этапа. Для этапа сборки вы можете использовать больший базовый образ со всеми необходимыми инструментами сборки, а затем использовать минимальный базовый образ для этапа выполнения, оптимизируя конечный размер образа.

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

Анатомия многоэтапного Dockerfile

Давайте глубже погрузимся в структуру многоэтапного Dockerfile и поймем его ключевые компоненты.

Разбор этапов

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

Важно отметить, что хотя этапы определяются последовательно в Dockerfile, фактическое выполнение независимых этапов может происходить параллельно. Docker автоматически обрабатывает эту параллелизацию и может значительно ускорить процесс сборки, особенно для сложных приложений с несколькими независимыми компонентами или зависимостями.

Например:

# Этап сборки фронтенда
FROM node:20 AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend ./
RUN npm run build

# Этап сборки бэкенда
FROM golang:1.22 AS backend-build
WORKDIR /app/backend
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend ./
RUN go build -o main .

# Финальный этап выполнения
FROM alpine:3.20
WORKDIR /app
COPY --from=frontend-build /app/frontend/dist ./frontend
COPY --from=backend-build /app/backend/main ./
CMD ["./main"]

В этом примере у нас есть три этапа: frontend-build для сборки фронтенд-ресурсов, backend-build для компиляции бэкенд-приложения и финальный этап выполнения, который объединяет артефакты из предыдущих этапов. Этапы frontend-build и backend-build могут быть собраны одновременно, так как они независимы.

Использование нескольких инструкций FROM

В многоэтапном Dockerfile вы встретите несколько инструкций FROM, каждая из которых обозначает начало нового этапа. Инструкция FROM указывает базовый образ для конкретного этапа. Например:

FROM node:20 AS frontend-build
# Инструкции этапа сборки фронтенда

FROM golang:1.22 AS backend-build
# Инструкции этапа сборки бэкенда

FROM alpine:3.20
# Инструкции финального этапа выполнения

Каждый этап использует разный базовый образ, подходящий для своей конкретной цели, например, node для сборки фронтенда, golang для сборки бэкенда и alpine для легковесной среды выполнения.

Копирование артефактов между этапами

Одной из ключевых особенностей многоэтапных сборок является возможность копировать артефакты из одного этапа в другой. Это достигается с помощью инструкции COPY --from. Она позволяет выборочно копировать файлы или каталоги из предыдущего этапа в текущий этап. Например:

COPY --from=frontend-build /app/frontend/dist ./frontend
COPY --from=backend-build /app/backend/main ./

Эти инструкции копируют собранные фронтенд-ресурсы из этапа frontend-build и скомпилированный бэкенд-бинарный файл из этапа backend-build в финальный этап выполнения.

Именование этапов для ясности

Чтобы улучшить читаемость и удобство обслуживания вашего многоэтапного Dockerfile, рекомендуется давать имена вашим этапам с помощью ключевого слова AS. Это позволяет ссылаться на конкретные этапы по имени при копировании артефактов или использовании их в качестве базы для последующих этапов. Например:

FROM node:20 AS frontend-build
# Инструкции этапа сборки фронтенда

FROM golang:1.22 AS backend-build
# Инструкции этапа сборки бэкенда

FROM alpine:3.20 AS runtime
COPY --from=frontend-build /app/frontend/dist ./frontend
COPY --from=backend-build /app/backend/main ./

Инструкции этапа выполнения

В этом примере этапы названы frontend-build, backend-build и runtime, что делает понятным, что представляет собой каждый этап, и позволяет легко ссылаться на них при копировании артефактов.

Понимая структуру многоэтапного Dockerfile и используя концепции этапов, множественные инструкции FROM, копирование артефактов между этапами и именование этапов для ясности, вы можете создавать хорошо структурированные и легко поддерживаемые многоэтапные сборки для ваших приложений.

Лучшие практики для многоэтапных сборок

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

Оптимизация порядка сборки

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

# Установка зависимостей
FROM node:20 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# Сборка приложения
FROM node:20 AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Финальный этап выполнения
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

В этом примере зависимости устанавливаются в отдельном этапе (deps), который идет перед этапом, собирающим приложение (build). Таким образом, если изменяется только код приложения, этап deps может быть повторно использован из кэша.

Использование более подходящего базового образа для каждого этапа

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

# Этап сборки
FROM golang:1.22 AS build
# Инструкции сборки

# Этап выполнения
FROM alpine:3.20
COPY --from=build /app/main ./

В этом примере этап сборки использует образ golang, который включает компилятор Go и инструменты, в то время как этап выполнения использует легковесный образ alpine, что приводит к меньшему конечному образу.

Заключение

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

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

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

Ссылка на оригинал статьи

Read more

Самохостинг (часть 2) - Динамический роутинг на Keenetic

Самохостинг (часть 2) - Динамический роутинг на Keenetic

Допустим у нас есть роутер Keenetic. Нам нужно, чтоб некоторые сайты грузились через поднятый на нем туннель (это может быть Wireguard, L2TP или даже банальный Socks5 proxy). Например, нас забанил Youtube по нашему внешнему IP адресу 😉, но мы все равно хотим его смотреть, да не на телефоне, а на нормальном

Самохостинг - стиль жизни

Самохостинг - стиль жизни

Я тут и тут писал про свой домашний сервер, но нигде не упоминал, что есть еще один сервер в ДЦ, где хостятся сайтики и кучка еще разных сервисов. Да и времени прошло с момента написания тех статей не мало. Сервер тот остался в другой стране и, как результат, все, что

Мониторинг долгих запросов PostgreSQL в Prometheus

Мониторинг долгих запросов PostgreSQL в Prometheus

Предположим, что у вас есть PostgreSQL (AWS RDS или классический PostgreSQL server), Prometheus, postgres exporter и alertmanager с Grafana. Стоит задача присылать уведомления о том, что в Postgres подвис запрос. Причина и т.п. нас мало интересует. Нужно просто сказать всем, кому положено, что есть проблема и ее нужно решить.

Почему я всё ещё люблю Fish Shell

Почему я всё ещё люблю Fish Shell

В 2017 году я написала о том, как сильно люблю Fish Shell, и спустя 7 лет ежедневного использования, я нашла ещё больше причин для восхищения. Поэтому решила написать новый пост, где соберу старые и новые причины моей любви к этой оболочке. Сегодня я задумалась об этом, потому что пыталась понять,