«1. Обзор

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

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

Начнем с использования официальных изображений.

2. Основывайте свое изображение на официальном

2.1. Что такое официальные изображения?

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

Давайте рассмотрим это более четко на примере, в котором используется официальный образ Nginx. Создатели веб-сервера поддерживают этот образ.

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

FROM nginx:1.19.2
COPY my-static-website/ /usr/share/nginx/html

Затем мы можем собрать наш образ:

$ docker build -t my-static-website .

И, наконец, запустить его:

$ docker run -p 8080:80 -d my-static-website

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

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

CMD ["nginx", "-g", "daemon off;"]

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

2.2. Образы, поддерживаемые их создателями

Хотя они и не являются официальными в том смысле, в котором они объяснялись выше, в Docker Hub есть другие образы, которые также поддерживаются создателями приложения.

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

Например, чтобы изменить порт по умолчанию, который прослушивает EMQX, мы можем добавить переменную среды EMQX_LISTENER__TCP__EXTERNAL: их программное обеспечение.

$ docker run -d -e EMQX_LISTENER__TCP__EXTERNAL=9999 -p 9999:9999 emqx/emqx:v4.1.3

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

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

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

3. Старайтесь не создавать новые образы, когда это возможно

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

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

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

Предположим, нам нужно настроить HAProxy для перенаправления запросов на соответствующие микросервисы в нашем приложении. Всю эту логику можно записать в один конфигурационный файл, скажем, my-config.cfg. Образ Docker требует, чтобы мы разместили эту конфигурацию по определенному пути.

Давайте посмотрим, как мы можем запустить HAProxy с нашей пользовательской конфигурацией, смонтированной на работающем контейнере:

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

$ docker run -d -v my-config.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro haproxy:2.2.2

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

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

4. Создавайте уменьшенные изображения

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

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

Давайте рассмотрим два простых способа уменьшить размер образа Docker.

4.1. Используйте тонкие версии, когда они доступны

Здесь у нас есть два основных варианта: тонкая версия Debian и дистрибутив Alpine Linux.

Тонкая версия — это попытка сообщества Debian удалить ненужные файлы из стандартного образа. Многие образы Docker уже являются упрощенными версиями, основанными на Debian.

Например, образы HAProxy и Nginx основаны на тонкой версии дистрибутива Debian debian:buster-slim. Благодаря этому эти образы уменьшились с сотен МБ до нескольких десятков МБ.

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

С другой стороны, многие образы предлагают версию Alpine, например образ Python, о котором мы упоминали ранее. Изображения на основе Alpine обычно имеют размер около 10 МБ.

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

Следует иметь в виду, что Alpine Linux несколько лет назад решил заменить системные библиотеки с более распространенной glibc на musl. Хотя большая часть программного обеспечения будет работать без проблем, нам стоит тщательно протестировать наше приложение, если мы выберем Alpine в качестве базового образа.

4.2. Используйте многоэтапные сборки

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

«Допустим, мы хотим использовать HAProxy и динамически настроить его с помощью REST API, Data Plane API. Поскольку этот двоичный файл API недоступен в базовом образе HAProxy, нам необходимо загрузить его во время сборки.

Мы можем загрузить бинарный файл HAProxy API на одном этапе и сделать его доступным на следующем:

Первый этап, downloadapi, загружает последнюю версию API и распаковывает tar-файл. Второй этап копирует двоичный файл, чтобы HAProxy мог использовать его позже. Нам не нужно удалять curl или удалять загруженный файл tar, так как первый этап полностью отбрасывается и не будет присутствовать в финальном образе.

FROM haproxy:2.2.2-alpine AS downloadapi
RUN apk add --no-cache curl
RUN curl -L https://github.com/haproxytech/dataplaneapi/releases/download/v2.1.0/dataplaneapi_2.1.0_Linux_x86_64.tar.gz --output api.tar.gz
RUN tar -xf api.tar.gz
RUN cp build/dataplaneapi /usr/local/bin/

FROM haproxy:2.2.2-alpine
COPY --from=downloadapi /usr/local/bin/dataplaneapi /usr/local/bin/dataplaneapi
...

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

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

5. Заключение

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

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

«