Оригинал: Dockerfile — Best Practices
Перевод для канала Мы ж программист
Написание готовых к производству Docker-файлов – не такая простая задача, как может показаться.
Этот сборник содержит несколько лучших практик написания Docker-файлов. Большинство из них взяты из опыта работы с Docker & Kubernetes. Это руководство, а не мандат – иногда могут быть причины не делать то, что здесь описано, но если вы не знаете, то, вероятно, именно это вам и следует делать.
Используйте официальные образы Docker, когда это возможно
Использование официальных образов Docker, предназначенных для вашей технологии, должно быть первым выбором. Почему? Потому что эти образы оптимизированы и протестированы МИЛЛИОНАМИ пользователей. Создание образа с нуля – хорошая идея, если официальный базовый образ содержит уязвимости или вы не можете найти базовый образ, предназначенный для вашей технологии.
Вместо того чтобы устанавливать SDK вручную:
FROM ubuntu
RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc \
| gpg --dearmor > microsoft.asc.gpg \
&& sudo mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/ \
&& wget -q https://packages.microsoft.com/config/ubuntu/18.04/prod.list \
&& sudo mv prod.list /etc/apt/sources.list.d/microsoft-prod.list \
&& sudo chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg \
&& sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list
RUN sudo apt-get install dotnet-sdk-3.1
Используйте официальный Docker Image:
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster
Alpine – не всегда лучший выбор
Несмотря на то, что Alpine имеет небольшой вес, известны некоторые проблемы с производительностью некоторых технологий (https://pythonspeed.com/articles/alpine-docker-python/).
Вторая особенность образов на основе Alpine – безопасность. Большинство сканеров уязвимостей не находят уязвимостей в образах на основе Alpine. Если сканер не нашел ни одной уязвимости, значит ли это, что образ на 100% безопасен? Конечно, нет.
Прежде чем принимать решение, оцените, в чем преимущества Alpine.
Ограничивайте количество слоев образа
Каждая инструкция RUN
в вашем Dockerfile завершается созданием дополнительного слоя (layer) в вашем финальном образе. Лучшей практикой является ограничивать количество слоев, чтобы сохранять легковесность образа.
Вместо:
RUN curl -SL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" --output nodejs.tar.gz
RUN echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c -
RUN tar -xzf "nodejs.tar.gz" -C /usr/local --strip-components=1
RUN rm nodejs.tar.gz
RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs
Попробуйте так:
RUN curl -SL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.gz" --output nodejs.tar.gz \
&& echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c - \
&& tar -xzf "nodejs.tar.gz" -C /usr/local --strip-components=1 \
&& rm nodejs.tar.gz \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
Не запускайте под пользователем root
Запуск контейнеров от имени пользователя, не являющегося пользователем root, существенно снижает риск повышения привилегий от контейнера к хосту. Это дополнительное преимущество безопасности (документация Docker).
Для образов на базе Debian удаление root из контейнера можно сделать следующим образом:
RUN groupadd -g 10001 dotnet && \
useradd -u 10000 -g dotnet dotnet \
&& chown -R dotnet:dotnet /app
USER dotnet:dotnet
ПРИМЕЧАНИЕ: Иногда, когда вы удаляете root из контейнера, вам нужно будет изменить разрешения приложений/сервисов.
Например, приложения dotnet не могут работать на порту 80 без привилегий root, и вам придется изменить порт по умолчанию (в примере это 5000).
ENV ASPNETCORE_URLS http://*:5000
Не используйте UID ниже 10000
UID (идентификаторы системных пользователей) ниже 10 000 – это риск для безопасности нескольких систем, потому что если кому-то удастся повысить привилегии за пределами контейнера Docker, его UID контейнера Docker может пересечься с UID более привилегированного пользователя системы, предоставив ему дополнительные полномочия. Для обеспечения максимальной безопасности всегда запускайте процессы с UID выше 10 000.
Используйте статичные UID и GID
В конечном итоге кому-то, кто будет работать с вашим контейнером, понадобится управлять правами доступа к файлам, принадлежащим вашему контейнеру. Если у вашего контейнера нет статического UID/GID, то необходимо извлечь эту информацию из запущенного контейнера, чтобы назначить правильные разрешения на файлы на хост-машине. Лучше всего использовать единый статический UID/GID для всех контейнеров, который никогда не меняется. Мы предлагаем 10000:10001
, чтобы chown 10000:10001 files/
всегда работал для контейнеров, следующих этим лучшим практикам.
Тег latest – зло, используйте конкретный тег образа
Используйте конкретную версию образа, используя major.minor
, а не major.minor.patch
, чтобы всегда быть уверенным в том, что:
- Поддерживается работоспособность ваших сборок (
latest
означает, что ваша сборка может произвольно сломаться в будущем, в то время какmajor.minor
должен означать, что этого не произойдет) - Получаете последние обновления безопасности, включенные в новые образы, которые вы собираете.
Почему вам, возможно, не стоит подписывать SHA
SHA pinning дает вам абсолютно надежные и воспроизводимые сборки, но это также, вероятно, означает, что у вас не будет очевидного способа извлечь важные исправления безопасности из базовых образов, которые вы используете. Если вы используете теги major.minor
, вы будете получать исправления безопасности случайно при сборке новых версий образа – ценой того, что сборки будут менее воспроизводимыми.
Рассмотрите возможность использования docker-lock: этот инструмент отслеживает, какой именно SHA образа вы используете для сборок, при этом фактический образ, который вы используете, по-прежнему имеет версию major.minor
. Это позволит вам воспроизводить ваши сборки так же, как если бы вы использовали SHA pinning, и при этом получать важные обновления безопасности, когда они выходят, как если бы вы использовали major.minor
версии.
Храните в CMD только аргументы
Если ваша команда указана в ENTRYPOINT
:
ENTRYPOINT ["/sbin/tini", "--", "myapp"]
И в CMD
передаются только аргументы для команды:
CMD ["--foo", "5", "--bar=10"]
Это позволяет людям передавать аргументы в исполняемый файл без необходимости выяснения его названия, то есть можно написать так:
docker run IMAGE --some-argument
Если же CMD
содержит название исполняемого файла, людям придётся догадываться, какое именно.
Всегда используйте COPY вместо ADD (есть одно исключение)
Единственное исключение для использования ADD
– необходимость автоматической разархивации:
ADD local-file.tar.xz /usr/share/files
Произвольные URL-адреса, указанные для ADD
, могут привести к MITM-атакам или источникам вредоносных данных. Кроме того, ADD
неявно распаковывает локальные архивы, что может не ожидаться и привести к обходу путей и уязвимостям Zip Slip.
Вам следует избегать подобных действий:
ADD https://example-url.com/file.tar.xz /usr/share/files
Подводя итог, COPY
следует использовать где возможно.
Совмещайте RUN apt-get update
и apt-get install
в единую команду
Использование в RUN
отдельной команды apt-get update
приводит к проблемам с кэшированием, и последующие инструкции apt-get install
не работают. Это связано с механизмом кэширования, который использует Docker. Во время сборки образа Docker воспринимает начальную и измененную инструкции как идентичные и повторно использует кэш предыдущих шагов. В результате apt-get update
не выполняется, потому что сборка использует кэшированную версию. Поскольку apt-get update
не выполняется, сборка потенциально может получить устаревшую версию пакетов.
Ниже приведен пример инструкции RUN
, которая демонстрирует все рекомендации apt-get.
RUN apt-get update && apt-get install -y \
curl \
git \
build-essential \
&& rm -rf /var/lib/apt/lists/*