Весной 2023 года разработчики Depot сообщили о том, что теперь с помощью их сервиса можно проверять Dockerfile при каждой сборке. Depot — это сервис удаленной сборки контейнеров, который может создавать образы Docker до 20 раз быстрее, чем сборка образов Docker внутри обычных CI-провайдеров. Это молодая компания, которая образовалась в начале 2022 года, но уже сейчас она состоит в венчурном фонде Y Combinator и привлекла 1,8 млн долларов инвестиций.
После добавления возможности проверять Dockerfiles в Depot разработчики сервиса столкнулись со множеством сложностей. В итоге они выделили 10 наиболее распространенных проблем при линтинге Dockerfile'ов и описали их в статье, которую мы перевели для вас.
В статье разработчики Depot разбирают каждую проблему, объясняют, почему она возникает и как ее решить. Авторы отмечают, что со временем список может измениться, но даже в таком виде он послужит хорошей отправной точкой для оптимизации Dockerfile'ов.
Линтинг Dockerfile'ов
В Depot мы используем два линтера Dockerfile'ов: hadolint и набор правил для линтера Dockerfile'ов, разработанный Semgrep. Благодаря им процесс становится более интеллектуальным и автоматизированным.
Hadolint можно запустить локально — задать ему определенные правила и снабдить конфиг-файлом. Также доступен пользовательский веб-интерфейс. Предварительно hadolint необходимо установить. Это можно сделать с помощью brew
либо использовать готовый Docker-образ и просто «скормить» ему Dockerfile:
hadolint Dockerfile
# или использовать Docker-образ
docker run --rm -i ghcr.io/hadolint/hadolint < Dockerfile
1. Объединяйте связанные инструкции RUN
В hadolint эта ошибка известна как DL3059. Это самая распространенная проблема, с которой мы сталкиваемся в своей практике. Она затрагивает почти 30% всех Dockerfile'ов. Суть в том, что несколько RUN
-инструкций следуют друг за другом, хотя их можно было бы объединить в одну. Пример:
RUN download_a_really_big_file
RUN remove_the_really_big_file
Чтобы понять, в чем тут дело, нужно вспомнить, как в Docker устроено кэширование слоев. Каждый новый оператор RUN
в Dockerfile — это отдельный слой в конечном образе.
То есть в примере выше один слой создается при загрузке большого файла и еще один — при его удалении. Оба слоя окажутся в конечном образе. То есть в него попадет первый слой с большим и лишним файлом и увеличит размер образа.
Другая проблема с DL3059 — установка пакетов в два захода. Например:
RUN fetch_package_registry_list
RUN install_some_package
В примере выше первый оператор RUN
получает список пакетов в реестре, второй — устанавливает нужный пакет. Проблема в том, что список пакетов в реестре может измениться, пока работает первый оператор. В итоге в образ попадет устаревший пакет.
Решение DL3059
При работе с большими файлами, которые добавляются и удаляются во время docker build
, разумно объединять эти операции в один атомарный оператор RUN
:
RUN download_a_really_big_file && \
remove_the_really_big_file
Это уменьшает конечный размер образа, поскольку удаляется промежуточный слой с большим файлом, ведь все манипуляции с ним проводятся в рамках одного RUN
-выражения. Если объединять RUN
-операторы, которые включают кэшируемые вещи, с вещами, которые часто обновляются (тем самым инвалидируя кэш), могут возникнуть тонкости при работе с кэшем. В таких случаях кэшируемую часть лучше держать в отдельном операторе RUN
.
В примере с реестром пакетов получение списка пакетов и установку нужного пакета можно совместить в одном операторе RUN
:
RUN fetch_package_registry_list && \
install_some_package
В результате перед установкой мы получим актуальный список пакетов из реестра, а значит, и у нужного пакета будет самая свежая версия.
2. Явно указывайте версии при установке с apt-get
Еще одна неоднозначная проблема с линтингом Dockfile'ов получила в hadolint номер DL3008. Она присутствует примерно в трети всех Dockerfile'ов. Проблема возникает, если при установке с помощью apt-get
не прописывать версии пакетов явно. Например:
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y some-package
Если не указывать версию явно, docker build
не будет ее проверять — так можно оказаться в ситуации, когда версия пакета отличается от нужной. Это может привести к проблемам при сборке Dockerfile'а или запуске получившегося образа.
Решение DL3008
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y some-package=1.2.*
Явно указывая версию пакета, мы заставляем build
скачать именно ее. Это позволяет контролировать, какие именно пакеты устанавливаются в Dockerfile, а также их зависимости.
Причина неоднозначного отношения к этой проблеме связана с тем, что привязка к конкретным версиям потенциально вредит обновлениям безопасности. Например, мы прописали версию пакета с уязвимостью. В этом случае, чтобы получить обновление безопасности, необходимо будет руками заменить ее на новую. Иначе Dockerfile так и будет собираться со старой. Именно поэтому так важно понимать, какие пакеты устанавливаются и какими последствиями для безопасности грозит явное указание версий.
3. Используйте --no-install-recommends, чтобы избежать установки ненужных пакетов
Еще одна распространенная ошибка линтера — DL3015: установка лишних пакетов с помощью apt-get
. Она присутствует в 22% всех Dockerfile'ов. Проблема возникает, когда apt-get install
запускается без флага --no-install-recommends
. Например:
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y some-package
Если не указан флаг --no-install-recommends
, apt-get
устанавливает рекомендуемые пакеты в дополнение к основному. То есть размер Docker-образа может сильно вырасти из-за установки в него лишних пакетов.
Решение DL3015
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y some-package --no-install-recommends
Решение очевидно: указывать флаг --no-install-recommends
в apt-get install
. Это предотвратит установку рекомендуемых пакетов и уменьшит размер образа. При этом необходимо понимать, какие именно пакеты идут в качестве рекомендуемых, чтобы не забыть о зависимостях.
4. Не используйте кэш при вызове pip install
Когда заходит речь о выполнении команды pip install
во время сборки, встает вопрос послойного кэширования. Соответствующая ошибка hadolint DL3042 присутствует в 18% всех Dockerfile'ов. Проблема возникает, когда мы забываем запретить pip install
использовать кэш в Dockerfile. Например:
FROM python:3.11
RUN pip3 install mysql-connector-python
В этом случае pip install
не только установит пакет, но и сохранит его в кэше. Если пакетов много, вырастет итоговый размер Docker-образа.
Решение DL3042
FROM python:3.11
RUN pip3 install --no-cache-dir mysql-connector-python
Нет никакого смысла кэшировать Pip-пакеты при сборке Docker-образа, поскольку их никто не будет переустанавливать. Вместо этого можно использовать кэш Docker-слоя. Отключение кэша уменьшает размер конечного образа.
5. Удаляйте списки apt-get после установки пакетов
Как уже говорилось в другой нашей статье, уменьшение размеров образов контейнеров часто связано с тем, как проходит docker build
. Ошибка hadolint DL3009 присутствует в 16% всех Dockerfile'ов. Проблема возникает, если после установки пакетов не удалить списки apt-get
. Например:
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y some-package --no-install-recommends
Приведенный выше пример для DL3015 можно еще больше оптимизировать, уменьшив итоговый размер образа. Если не очистить кэш apt-get
, тот попадет в слой для RUN
, занимая ценное место.
Решение DL3009
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y some-package --no-install-recommends && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Здесь установка пакета объединяется с очисткой кэша apt-get
— обе операции проходят в одном атомарном RUN
-выражении. Это позволяет уменьшить размер конечного образа, удалив из него кэш apt-get
, и избежать добавления лишних слоев.
6. Используйте WORKDIR вместо RUN cd some-path
Другая распространенная проблема линтинга Dockerfile'ов идет под номером DL3003. Она возникает, когда вместо WORKDIR
используется RUN cd
. Ошибка встречается в 14% всех Dockerfile'ов. Типичный пример:
FROM ubuntu:22.04
RUN cd /usr/src/app && git clone git@github.com:depot/some-repo.git
Каждое RUN
-выражение выполняется в собственной командной оболочке, и большинство команд умеют работать с абсолютными путями.
Решение DL3003
FROM ubuntu:22.04
WORKDIR /usr/src/app
RUN git clone git@github.com:depot/some-repo.git
При смене директорий можно использовать WORKDIR
, который запускает shell
в указанной директории. Единственное исключение — когда нужно сделать что-то в подоболочке. В этом случае придется использовать cd
.
7. Явно указывайте версии пакетов при установке через pip
Ошибка DL3013 повторяет логику DL3008 в применении к pip
вместо apt-get install
. Ошибка встречается в 13% всех Dockerfile'ов. Типичный пример:
FROM python:3.11
RUN pip3 install --no-cache-dir mysql-connector-python
Если не указывать версию явно, docker build
не будет ее проверять, и тогда вы рискуете оказаться в ситуации, когда версия пакета отличается от нужной. Как говорилось в DL3008, если установить версию, отличную от изначально заданной при создании Dockerfile, это может привести к неожиданному поведению.
Решение DL3013
FROM python:3.11
RUN pip3 install --no-cache-dir mysql-connector-python==8.1.0
Если явно указать версию mysql-connector-python
, docker build
будет вынуждена использовать именно ее независимо от того, что есть в кэше слоя Docker.
8. Используйте нотацию JSON для аргументов CMD и ENTRYPOINT
Ошибка линтинга DL3025 сводится к корректности при запуске образа. Она присутствует в 12% всех Dockerfile'ов. Вот типичные примеры выражений, в которых она встречается:
FROM ubuntu:22.04
ENTRYPOINT foo run-server
FROM ubuntu:22.04
CMD foo run-server
Если не использовать JSON-нотацию для аргументов CMD
и ENTRYPOINT
, исполняемые файлы не будут получать сигналы от ОС. Это особенно актуально, когда необходимо сообщить работающему контейнеру об окончании его работы — послать SIGTERM
.
Решение DL3025
FROM ubuntu:22.04
ENTRYPOINT ["foo", "run-server"]
FROM ubuntu:22.04
CMD ["foo", "run-server"]
Если использовать JSON-нотацию, исполняемый файл станет PID 1 в контейнере и сможет получать сигналы от ОС. Еще пара моментов, на которые следует обратить внимание:
CMD
не обрабатывает переменные окружения в shell-формате (например,$FOO_BAR
) из-за побочного эффекта, связанного с использованиемsh -c
в качестве точки входа по умолчанию. Так что необходимо самостоятельно обрабатывать переменные окружения внеCMD
-выражения.
CMD
-выражение парсируется как массив JSON, поэтому для передачи аргументов необходимо использовать двойные кавычки (" ") вместо одинарных (' ').
9. Используйте apt-get или apt-cache вместо apt
Команда apt
предназначена для конечного пользователя и не должна использоваться в RUN
-выражениях Dockerfile'ов. Ошибка линтинга DL3027 означает, что в Dockerfile вместо apt-get
или apt-cache
используется apt
. Она встречается в 9% всех Dockerfile'ов. Типичный пример:
FROM ubuntu:22.04
RUN apt install -y some-package=1.2.*
Решение DL3027
FROM ubuntu:22.04
RUN apt-get install -y some-package=1.2.*
apt
может по-разному вести себя в дистрибутивах Linux. Поэтому лучше использовать более базовые apt-get
или apt-cache
.
10. Явно задавайте версии пакетов при установке через apk add
Как отмечалось в DL3008 и DL3013, при установке пакетов следует явно указывать их версии. Это справедливо и для apk add
в Dockerfile'ах на основе Alpine. Ошибка встречается в 8% всех Dockerfile'ов. Типичный пример:
Решение DL3018
FROM alpine:3.7
RUN apk --no-cache add some-package=~1.2.3
Обоснование то же самое: если версия указана явно, docker build
извлечет именно ее независимо от того, что есть в кэше Docker-слоя. Следует отметить, что для образов на базе Alpine используется частичная привязка с помощью ~
. Можно привязать и к конкретной версии, задав ее в виде some-package=1.2.3
. Однако, если пакет удален, сборка завершится с ошибкой.
Заключение
В этой статье были рассмотрены 10 самых распространенных проблем с линтингом Dockerfile'ов, с которыми мы сталкиваемся в своей практике. Они различаются по серьезности и последствиям. У каждой проблемы есть свои нюансы, но, если устранить их, вы сможете оптимизировать Dockerfile'ы и ускорить сборку образов.
Например, явное указание версий гарантирует определенное состояние при сборке Docker-образов, но его недостаток — потенциальные сложности с получением обновлений безопасности. Использование --no-install-recommends
позволяет избежать лишних зависимостей, которые не нужны или не используются. Минус — можно упустить зависимость, которая необходима.
Надеемся, эта статья подкинула вам пару идей о том, как улучшить Dockerfile’ы и как с этим может помочь линтинг. Если хотите больше узнать о том, чем занимается Depot, рекомендуем ознакомиться с нашей недавней публикацией о линтинге и создании Dockerfile'ов.
P. S.
Читайте также в нашем блоге: