Оригинал: Dockerfile — Best Practices

Перевод для канала Мы ж программист

Написание готовых к производству Docker-файлов – не такая простая задача, как может показаться.

Этот сборник содержит несколько лучших практик написания Docker-файлов. Большинство из них взяты из опыта работы с Docker & Kubernetes. Это руководство, а не мандат – иногда могут быть причины не делать то, что здесь описано, но если вы не знаете, то, вероятно, именно это вам и следует делать.

Используйте официальные образы Docker, когда это возможно

Использование официальных образов Docker, предназначенных для вашей технологии, должно быть первым выбором. Почему? Потому что эти образы оптимизированы и протестированы МИЛЛИОНАМИ пользователей. Создание образа с нуля – хорошая идея, если официальный базовый образ содержит уязвимости или вы не можете найти базовый образ, предназначенный для вашей технологии.

Вместо того чтобы устанавливать SDK вручную:

Dockerfile
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:

Dockerfile
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) в вашем финальном образе. Лучшей практикой является ограничивать количество слоев, чтобы сохранять легковесность образа.

Вместо:

Dockerfile
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

Попробуйте так:

Dockerfile
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 из контейнера можно сделать следующим образом:

Dockerfile
RUN groupadd -g 10001 dotnet && \
   useradd -u 10000 -g dotnet dotnet \
   && chown -R dotnet:dotnet /app
   
USER dotnet:dotnet

ПРИМЕЧАНИЕ: Иногда, когда вы удаляете root из контейнера, вам нужно будет изменить разрешения приложений/сервисов.

Например, приложения dotnet не могут работать на порту 80 без привилегий root, и вам придется изменить порт по умолчанию (в примере это 5000).

Dockerfile
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:

Dockerfile
ENTRYPOINT ["/sbin/tini", "--", "myapp"]

И в CMD передаются только аргументы для команды:

Dockerfile
CMD ["--foo", "5", "--bar=10"]

Это позволяет людям передавать аргументы в исполняемый файл без необходимости выяснения его названия, то есть можно написать так:

Bash
docker run IMAGE --some-argument

Если же CMD содержит название исполняемого файла, людям придётся догадываться, какое именно.

Всегда используйте COPY вместо ADD (есть одно исключение)

Единственное исключение для использования ADD – необходимость автоматической разархивации:

Dockerfile
ADD local-file.tar.xz /usr/share/files

Произвольные URL-адреса, указанные для ADD, могут привести к MITM-атакам или источникам вредоносных данных. Кроме того, ADD неявно распаковывает локальные архивы, что может не ожидаться и привести к обходу путей и уязвимостям Zip Slip.

Вам следует избегать подобных действий:

Dockerfile
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.

Dockerfile
RUN apt-get update && apt-get install -y \
    curl \
    git \
    build-essential  \
    && rm -rf /var/lib/apt/lists/*