docker - как уменьшить размер образа

Некоторые замечания и соображения по тому, как уменьшить общий размер образа, создаваемого Docker. Размер образа (image) может легко превысить несколько гигабайт, даже у самого простого приложения, ведь мы создаем  окружение, а значит  включаем  в образ большое количество зависимостей. При активном выпуске обновлений/релизов приложения это может стать значительной проблемой и потребовать контроля за расходованием дискового пространства, особенно на серверах где место лимитировано.

#1 Используем базовые Docker-образы меньших размеров

Разные образы могут предоставлять одинаковую функциональность, но быть основаны на разных операционных системах и редакциях. Например есть выбрать в качестве основы FROM ubuntu, то мы получим сразу более 100MB, а если выбрать FROM alpine, то начальный размер будет порядка 5MB. То же касается и других популярных образов.

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

#2 Не устанавливаем ненужные программы и приложения

Стараемся не устанавливает "лишние" приложения, которые реально не используются в работе приложений. Если всё же без этого не обойтись, то разделяем сборки образов на development и production, используем разные Dockerfile-файлы. Тогда в образе разработки мы можем позволить себе больше, а образ для работы приложения будет держать в тонусе.

#3 Уменьшаем размеры "слоёв"

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

Комбинируем одинаковые по смыслу RUN инструкции

ENV COMMON_PACKAGES bash \
  curl \
  build-base \
  tzdata \
  libxml2-dev \
  postgresql-dev \
  curl-dev sqlite-dev \
  libsass \
  libsass-dev \
  gcompat \
  libc6-compat \
  nodejs \
  npm \
  yarn \
  imagemagick \
  git \
  icu-dev

RUN set -xe \
  && apk update \
  && apk upgrade \
  && apk add --update ${COMMON_PACKAGES}
  

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

#4 Не устанавливаем рекомендованные пакеты

Менеджер apt, apt-get могут предлагать и устанавливает рекомендованные пакеты, мы же явно отказываемся от этой услуги

RUN set -xe \
  && apt-get update -qq \
  && apt-get install --no-install-recommends -y apt-transport-https curl ca-certificates python3  python3-distutils

для apk добавляем --no-cache

RUN set -xe \
  && apk update \
  && apk upgrade \
  && apk add --no-cache --update ${COMMON_PACKAGES} \
  && rm -rf /var/cache/apk/* /tmp/* /var/tmp/*

#5 удаляем артефакты работы apt, apk

Удаляем временные директории, использовавшиеся в работе apt, apk и очищаем  кеш

для apk

RUN set -xe \
  && apk update \
  && apk upgrade \
  && apk add --no-cache --update ${COMMON_PACKAGES} \
  && rm -rf /var/cache/apk/* /tmp/* /var/tmp/*

для apt

RUN set -xe \
  && apt-get update -qq \
  && apt-get install --no-install-recommends -y ${COMMON_PACKAGES} \
  && rm -rf /var/lib/apt/lists/*
Бонус! Используем [FromLatest.io](https://fromlatest.io) для оценки своего Dockerfile на предмет оптимизации.

docker multistage-build

А это самое интересное и полезное.

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

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

Пример Rails приложения

# 1 =====================================
# специальный образ, который содержит все необходимые 
# для сборки библиотеки и приложения
# размер образа ~730mb
FROM diproart/ruby:2.6.4-alpine3.10 AS builder

# полный на2ор пакетов
ENV COMMON_PACKAGES \
	tzdata \
    ...
    
ENV ..

RUN set -xe \
	&& apk update \
    && apk upgrade \
    && apk add --no-cache --update ${COMMON_PACKAGES} \
    && rm -rf /var/cache/apk/* /tmp/* /var/tmp/*

# additional clean
#RUN rm -rf /usr/local/bundle/cache/*gem     

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY . .
RUN 

# 2 =====================================
# чистый образ, в который добавим только самое необходимое
# размер образа ~51mb
FROM ruby:2.6.4-alpine3.10

# только необходимые для работы 
# в production пакеты
ENV PRODUCTION_PACKAGES \
	tzdata \
    ...

ENV ...

RUN set -xe \
	&& apk update \
    && apk upgrade \
    && apk add --no-cache --update ${PRODUCTION_PACKAGES} \
    && rm -rf /var/cache/apk/* /tmp/* /var/tmp/*

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

WORKDIR /usr/src/app

# копируем скомпилированное приложение
# и пакеты в напрямую в образ из предыдущего шага
COPY --from=builder /usr/src/app .
COPY --from=builder /usr/local/bundle/ /usr/local/bundle/
   
EXPOSE 3000
ENTRYPOINT [ "bundle", "exec" ]
CMD [ "rails", "s", "-b", "0.0.0.0" ]

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