Пост

Работа с образами в Docker. Часть 1

Уменьшение итогового размера и увеличесние скорости сборки образов Docker

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

В локальном хранилище образов было немало тяжеловесов на основе базовых образов систем, например, python3.10-bookworm размером под 800 Мб или golang:1.22.4-bookworm под 1.2 Гб. По их содержимому даже пробегаться страшно - помимо установленных дополнительно, полный набор пакетов и зависимостей, поставляемых с основной системой, причем давно устаревших, а значит потенциально уязвимых.

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

  • Выбор специализированных урезанных образов против образов, основанных на полных дистрибутивах систем

✖️ slim - уменьшенная версия стандартного образа системы с удалением ненужных пакетов, компонентов, зависимостей.

✖️ alpine - образ на основе облегченного, минималистичного дистрибутива Alpine Linux. Еще меньше, чем slim, использует специальные версии стандартной библиотеки musl libc и Busybox для пущей миниатюрности.

✖️ scratch - образ-пустышка. Идеальный вариант для запуска приложений, не требующих никакого окружения, внешних зависимостей и т.д.

  • Многоэтапная сборка

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

  • Исключение лишних данных из сборочного процесса с помощью .dockerignore

  • Использование официальных образов

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

⚒️ Пример двухэтапной сборки python приложения с итоговой публикацией в slim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# собираем зависимости
FROM python:3.10 AS builder
COPY requirements.txt .

RUN pip install --user -r requirements.txt

# помещаем исходники и собранные зависимости в итоговый образ
FROM python:3.10-slim

COPY --from=builder /root/.local /root/.local
COPY . /myapp

WORKDIR /myapp

ENV PATH=/root/.local/bin:$PATH

CMD ["python", "-u", "-m", "src"]

💥 В итоге получился образ размером 165 Мб, содержащий 135 установленных пакетов и 814 исполняемых файлов. Не такой уж и чистый, но по сравнению со стандартным образом, значительно меньше.

⚒️ Пример трехэтапной сборки golang приложения с итоговой публикацией в scratch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# кэширование модулей
FROM golang:1.22.4-alpine3.20 AS modules
COPY go.mod go.sum /modules/
WORKDIR /modules
RUN go mod download

# сборка приложения
FROM golang:1.22.4-alpine3.20 AS builder
COPY --from=modules /go/pkg /go/pkg
COPY . /app
WORKDIR /app
RUN go build -o /bin/myapp ./cmd/myapp

# публикация
FROM scratch
COPY --from=builder /app/config /config
COPY --from=builder /bin/myapp /myapp
CMD ["/myapp"]

💥 Размер полученного образа 29 Мб. Содержит 38 модулей проекта и 1 исполняемый файл.

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

Больше полезной информации в Telegram-канале