Docker для новичков — #2 Все инструкции Dockerfile
Эта публикация — текстовый вариант и сценарий для видео на YouTube (оно удобно разбито на эпизоды).
Привет, сегодня я расскажу о том что такое Dockerfile, из чего он состоит и как его написать.
С помощью Dockerfile можно создавать image. Docker автоматически создает image читая инструкции из этого файла. С помощью Dockerfile вы описываете то как ваше приложение будет работать внутри контейнера. Это основная задача Dockerfile.
Image можно представить как слоеный пирог, где некоторые инструкции добавляет новый слой. Каждый слой занимает какой-то объем памяти, поэтому когда вы пишите Dockerfile, необходимо использовать инструкции FROM, RUN, COPY, ADD рационально. Именно эти инструкции и создают слои в итоговом image.
Build Context
Давайте сначала разберемся в том что такое Build context.
Когда мы вызываем команды docker build, Docker создает image на основе Dockerfile и build context.
Build context — это набор файлов, к которым есть доступ во время построения image.
При вызове команды docker build, можно передать локальную директорию, в которой находится Dockerfile, tar-архив, удаленный git-репозиторий или же сам текст Dockerfile переданный прямо в консоль.
В этом случае build context является локальной директорией, удаленным Git репозиторием или tar архивом, к которым можно получить доступ во время сборки. COPY и ADD инструкции могут обратиться к любому из файлов и директорий внутри этого контекста.
Все поддиректории включаются в контекст тоже.
Если вы передаете Dockerfile текстом в команду build, то тогда он интерпретируется как Dockerfile и Docker не использует никакие другие файлы из контекста.
Как и с Git, можно указать .dockerignore файл и указанные файлы или директории из контекста не будут доступны Docker для копирования в image.
Вы можете сделать несколько Dockerfile, назвать их по разному и они будут работать. Соответствующе назвав .dockerignore Docker учтет и его.
Инструкции Dockerfile
Dockerfile — набор комментариев, инструкций и аргументов к ним.
Инструкция не чувствительна к регистру, но принято писать ее в верхнем регистре, как и SQL запросы для того, чтобы было проще отличить от аргументов.
Docker выполняет инструкции из Dockerfile по порядку.
Dockerfile должен начинаться с инструкции FROM. Эта инструкция определяет родительский image, на основе которого строится данный image. Все image должны начинаться с какого-нибудь базового image. Чтобы начать вообще с минимума, используйте базовый image alpine — всего 5 мб и работающий линукс. В других случаях вам понадобится использовать уже существующий image.
Например, когда мы хотим контейнеризовать Spring Boot приложение, нам необходимо с помощью Maven установить зависимости, собрать исполняемый jar файл, а потом уже запустить его с помощью jdk.
Так будет выглядеть этот image.
Мы воспользовались уже существующими image maven и openjdk. И для контейнеризации приложения понадобилось лишь несколько собственных инструкций.
В этом примере вообще две инструкции FROM, их может быть несколько, если сборка image — это многошаговый процесс.
Перед первой инструкцией FROM могут находиться только комментарии, директивы парсера и инструкция ARG с описанием аргументов.
Все другие инструкции связаны с изменением image, поэтому они не смогут сработать до определения базы, которую они должны изменять.
FROM [--platform=
] [AS ]
FROM [--platform=
] [: ] [AS ]
FROM [--platform=
] [@ ] [AS ]
Image должен быть валидным и он может спулиться с DockerHub или другого публичного репозитория во время сборки.
FROM может использоваться несколько раз, если сборка представляет многошаговый процесс, тогда каждому шагу можно дать имя с помощью AS name в инструкции FROM.
Можно воспользоваться COPY –from=
Tag или digest необязательны, если вы не указываете их, тогда Docker пытается найти тег latest и выбрасывает ошибку если предоставленный тег не найден.
Может быть применен дополнительный тег –platform, чтобы указать платформу image, например linux/amd64 или windows/amd64. По умолчанию используется платформа, на которой собирается image.
Комментарии
Docker считает строки, которые начинаются с # как комментарии.
В другие местах # считается аргументом. То есть вы не можете написать комментарий с середины строки. Комментарии однострочные, и если хотите продолжить, то нужно указать решетку еще раз.
Перед исполнением Dockerfile комментарии удаляются, поэтому они никак не влияют на построение image.
Табуляция и пробелы перед инструкциями и комментариями допускаются, но не рекомендуются.
Эти примеры будет работать одинаково.
Изображение взято из документации Docker.
Директивы парсера
Директивы парсера не обязательны. Директивы не добавляют слои в image. Директивы представляются как комментарий, который выглядит как ключ=значение. Одна директива может использоваться лишь раз. Она не чувствительна к регистру и может иметь пробелы и табуляцию между знаком решетки и названием директивы.
После первой встречной инструкции или комментария или пустой строки директивы перестают быть директивами и становятся комментариями. Поэтому они должны быть написаны подряд в начале Dockerfile без прерываний.
Директивы из этих примеров не будут работать из-за нарушения правил.
Она не может быть разделена на две строки, не может повторяться, должна находиться до первой инструкции и комментария. Неизвестная директива воспринимается как комментарий.
Изображение взято из документации Docker.
Изображение взято из документации Docker.
Есть две директивы парсера — syntax и escape
Syntax используется при сборке с помощью BuildKit, это не обычный клиент, поэтому я не буду рассказывать про нее.
Escape обозначает символ конца строки в Dockerfile. По умолчанию это \.
Обычно в качестве разделителя на Windows используется апостроф `, где \ является разделителем директорий.
Аргументы
Мы помним, что аргументы могут быть указаны до инструкции FROM, они также могут быть указаны и после инструкции FROM.
Инструкция аргумента выглядит как:
ARG
[= ]
Значение по умолчанию можно опустить.
Эта инструкция определяет те аргументы, которые пользователь может передать в команду build, во время сборки image при помощи флага --build-arg
Например, вы можете передать версию базового image или название директории, в которой будет происходить какая-нибудь логика.
Значение аргумента и переменной можно получить при помощи знака доллара с названием переменной, либо же обернутого в фигурные скобки названия переменной. Поддерживаются модификаторы, которые позволяют менять значение.
Например, если вы указываете ${argname:-word}, то если аргумент argname был передан, его значение будет использовано, иначе слово word.
Если вы указываете ${argname:+word}, тогда если аргумент был передан, будет использоваться слово word, иначе пустая строка.
Это же работает и с переменными. Переменные можно получать на основе других переменных и аргументов с помощью обращения по названию.
Аргумент доступен в инструкциях после его упоминания. То есть в данном примере во второй строке user будет иметь значение some_user, потому что аргумент не определен еще на тот момент, а в четвертой уже переданный username
Изображение взято из документации Docker.
Также если вы хотите использовать один и тот же аргумент в нескольких стадиях сборки, то необходимо в каждой из них определять аргумент.
Изображение взято из документации Docker.
Не передавайте секреты и пароли с помощью аргументов, потому что они будут видны в docker history. Для этого нужно использовать инструкцию RUN –mount=type=secret. Об этом я расскажу позже.
Переменные
Инструкция выглядит следующим образом:
ENV
= …
При вызове docker build или docker run можно переопределить переменные с помощью флага –env.
По сути эта инструкция работает как и аргументы. Но есть несколько отличий.
Вы можете указывать несколько переменных в одной строке. Если значение включает пробелы, то нужно обернуть его в двойные кавычки.
В отличие от аргументов, переменные хранятся в image и доступны для просмотра с помощью docker inspect или в Docker Desktop.
Переменные наследуются из родительского image.
Значения переменных доступны после конца инструкции, то есть в таком случае значение def будет равно hello, а ghi будет равно bye.
Изображение взято из документации Docker.
Если хотите, чтобы значение переменной использовалось лишь во время сборки, тогда установите значение переменной при вызове команды RUN. Либо же воспользуйтесь инструкцией ARG.
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y …
WORKDIR
Инструкция WORKDIR устанавливает рабочую директорию для выполнения следующих команд.
WORKDIR /path/to/workdir
По умолчанию рабочая директория это корень файловой системы. Вам может понадобиться конкретная папка, поэтому можно использовать WORKDIR.
Изображение взято из документации Docker.
Эта инструкция может быть вызвана несколько раз. Если в начале пути стоит /, тогда путь к рабочей директории будет абсолютный, а если / нет, то относительный текущего.
Тут работает подстановка аргументов и переменных, поэтому в данном случае рабочая директория будет path/$DIRNAME
Рабочая директория наследуется от базового image, поэтому рекомендуется устанавливать ее явно при описании своего Dockerfile.
RUN
Инструкция RUN имеет два вида:
RUN
, выполняется в консоли /bin/sh -c или cmd /S /C (shell form)
RUN [«executable», «param1», «param2»] (exec form)
Команда RUN создает новый слой в текущем image. Соответственно этот обновленный image будет использоваться во всех инструкциях далее.
С помощью exec формы возможно использовать другую консоль и выполнять команды в базовом image, а не измененном.
Exec форма принимает как аргумент массив JSON, следует использовать двойные кавычки, а не одиночные.
Exec форма не вызывает консоль напрямую, поэтому нужно передавать первым аргументом необходимую консоль. Также при использовании данной формы используются локальные переменные консоли, а не Docker.
RUN –mount=[type=
]
Этот флаг позволяет создать файловую систему, которая будет доступна во время сборки image.
Это может понадобиться для доступа к файлам на хосте, обращению к секретам или использования кэша для ускорения сборки.
Типы mount:
Bind — default, readonly
Cache — временная директория для кэша для компиляторов и пакетных менеджеров
Secret — позволяет Docker получить доступ к секретным файлам без копирования их в image
Ssh — позволяет Docker получить доступ к SSH ключам через SSH агентов
CMD
У инструкции CMD есть три формы:
Изображение взято из документации Docker.
В Dockerfile может быть лишь одна инструкция CMD, иначе только последняя будет выполнена.
Основная задача инструкции CMD — предоставить действие по умолчанию для исполняющегося контейнера. Она может начинаться с выполняемой команды, а может и не начинаться, но тогда нужно указать инструкцию Entrypoint и обе инструкции должны быть в json формате — с двойными кавычками.
Как и для RUN, exec форма не вызывает командную строку, а shell форма вызывает. Поэтому если хотите использовать консоль, то либо используйте shell форму, либо передавайте явно консоль.
Если вы передадите аргументы в docker run, тогда они переопределят те аргументы из инструкции CMD.
Разница между RUN и CMD в том, что RUN выполняется во время сборки и создает новый слой в image, а CMD не выполняется во время сборки, но исполняется при запуске контейнера.
ENTRYPOINT
Давайте рассмотрим инструкцию ENTRYPOINT, которая используется в паре с CMD.
У нее есть две формы:
Exec форма ENTRYPOINT [«executable», «param1», «param2»] — предпочитаемая
Shell форма ENTRYPOINT command param1 param2
ENTRYPOINT позволяет вам сконфигурировать контейнер, который будет работать как исполняемый, то есть указать команду, которая выполнится при старте контейнера.
Можно переопределить entrypoint, если вызвать docker run –entrypoint
Как и CMD только последняя инструкция ENTRYPOINT будет исполнена
Есть несколько правил работы CMD и ENTRYPOINT:
В Dockerfile должен быть определен как минимум CMD или ENTRYPOINT или обе инструкции
ENTRYPOINT должна быть использована когда контейнер используется как исполняемое приложение
CMD должна быть использована для определения аругментов по умолчанию для ENTRYPOINT
CMD будет перезаписан, когда контейнер будет запущен с другими аргументами
Изображение взято из документации Docker.
Если CMD был определен в базовом image, то он не наследуется, поэтому его надо будет переопределять в текущем image.
LABEL
LABEL
= = = …
Инструкция LABEL добавляет метаданные в image, это ключ-значение. Тут например, можно хранить информацию о версии приложения, каких-либо других параметрах.
Можно переносить аргументы на новые строки, либо же писать на одной либо же писать несколько инструкций.
Необходимо использовать двойные кавычки, а не одиночные, иначе не вызовется подстановка переменных.
LABEL example=«foo-$ENV_VAR»
Лейблы с базовых image наследуются и переопределяются, если указан тот же ключ.
Чтобы увидеть все лейблы image, используйте docker image inspect.
EXPOSE
EXPOSE
[ / …]
Инструкция EXPOSE информирует Docker о том, что контейнер слушает определенный порт, когда он запущен. Можно указать TCP либо UDP соединение, по умолчанию TCP.
EXPOSE не открывает порт наружу на самом деле. Он лишь информирует пользователя image о работающих портах. Чтобы получить доступ к порту контейнера нужно явно указать флаг -p при создании контейнера.
Если контейнеры работают в одной сети, то они могут обращаться друг к другу без раскрытия порта хосту, что делает работу сети контейнеров более безопасной.
Вы можете создать контейнер базы и контейнер приложения, разместить их в одной сети, и приложение сможет обращаться к базе, в то время как снаружи Docker вы не сможете попадать в базу без явного раскрытия портов.
ADD
ADD [--chown=
: ] [--chmod= ] [--checksum= ] …
ADD [--chown=
: ] [--chmod= ] [» »,…» »]
У инструкции ADD есть две формы. Она копирует файлы, директории и удаленные URLs, и добавляет их в файловую систему image.
Можно использовать wildcard из Golang filepath.Match, чтобы копировать несколько файлов, подходящих под этот паттерн.
ADD hom* /mydir/
ADD hom?.txt /mydir/
Указав относительный путь, файл будет добавлен в WORKDIR/relativeDir
ADD test.txt relativeDir/
Указав абсолютный путь, файл будет добавлен в корень
ADD test.txt /absoluteDir/
Есть несколько правил:
должен быть внутри build контекста, нельзя добавить файлы из вне контекста Директория не копируется, копируется только содержимое
Если
это директория, то все ее содержимое копируется Если
это архив, тогда он распаковывается Если
не существует, то он создается со всеми нужными путями для него
С помощью этой инструкции можно провалидировать хэшсумму файла.
COPY
COPY [--chown=
: ] [--chmod= ] …
COPY [--chown=
: ] [--chmod= ] [» »,…» »]
Инструкция COPY копирует файлы и директории из
Как и в ADD поддерживаются wildcard, логика абсолютных и относительных путей. Эти инструкции схожи между собой, но есть несколько отличий.
ADD поддерживает URL
ADD автоматически извлекает локальные tar-архивы
COPY принимает только локальные файлы
Рекомендуется использовать COPY, так как ADD предоставляет дополнительные функции, которые следует использовать с осторожностью.
VOLUME
VOLUME [»/data»]
Volumes используются для того, чтобы разделять одну директорию между хостом, на котором работает Docker и контейнером. Так, например, можно сохранять данные базы данных и они не будут очищаться при перезапуске контейнера.
С помощью этой инструкции можно сказать Docker о том, что необходимо сохранить некоторую директорию с вложенными файлами. И тогда при старте контейнера будет создан volume, соответствующий этой директории.
Несколько особенностей volumes:
При работе на Windows, volume должен быть несуществующей или пустой директорией и это не может быть диск С
Если на каких-нибудь шагах построения image содержимое volume меняется, то эти изменения не будут сохранены
Нужно использовать двойные кавычки как и везде, так как аргумент инструкции — это массив JSON строк
Директория на хосте, которая будет хранить данные волюма зависит от самого хоста и определяется на момент создания и старта контейнера.
USER
USER
[: USER UID[: GID]]
Инструкция USER необходима для установки имени пользователя и группы или их id для использования по умолчанию. Этот user доступен в инструкциях RUN, ENTRYPOINT и CMD.
Если у юзера нет определенной группы, то он будет в группе root.
Изображение взято из документации Docker.
В этом примере создается новый пользователь Патрик и он в дальнейшем используется в ходе построения image.
STOPSIGNAL
STOPSIGNAL signal
Инструкция STOPSIGNAL определяет сигнал, который будет отправлен контейнеру для его остановки. Это может быть полезно, если ваше приложение должно получать другой сигнал. Стопсигнал может быть переопределен при создании или запуске контейнера.
ONBUILD
ONBUILD INSTRUCTION
Инструкция ONBUILD добавляет триггер, который будет вызван, когда этот image будет использоваться как базовый для другого image. Он будет исполнен при создании дочернего контейнера как будто бы инструкция вставлена после FROM.
Это полезно, когда ваш image должен быть построен поверх какого-то другого image.
Это работает в такой последовательноcти:
Когда инструкция ONBUILD исполняется, она добавляется в metadata этого image, она не влияет на сам image, только на дочерние
После конца сборки этого image список всех триггеров доступен под ключом OnBuild и его можно посмотреть с помощью docker inspect
Когда этот image будет использован в FROM при построении другого image, docker находит эти триггеры и исполняет их в том же порядке, в каком они были добавлены. Если какой-нибудь триггер падает, то и сборка всего image падает.
Триггеры очищаются после сборки дочернего image, таким образом они не наследуются дальше первого уровня.
Изображение взято из документации Docker.
Например, мы делаем свой python-builder, который обрабатывает исходный код пользователей, добавив это в наш image, мы создадим два триггера. И когда пользователь воспользуется нашим image как родительским, то все файлы из его контекста добавятся в папку /app/src и запустится скрипт python-build.
ONBUILD инструкции не могут включать инструкции ONBUILD, FROM и MAINTAINER.
HEALTHCHECK
Инструкция HEALTHCHECK нужна для проверки работоспособности контейнера. Благодаря ей, Docker может перезапускать упавшие контейнеры и управлять жизненным циклом контейнера. Например, если ваш сервер попал в бесконечный цикл и не отвечает, с помощью HEALTHCHECK это можно определить.
Эта инструкция имеет две формы:
HEALTHCHECK [OPTIONS] CMD command
HEALTHCHECK NONE
В качестве опций можно передать:
--interval=DURATION (default: 30s)
--timeout=DURATION (default: 30s)
--start-period=DURATION (default: 0s)
--start-interval=DURATION (default: 5s)
--retries=N (default: 3)
Первая проверка пройдет через время указанное в interval, и в дальнейшем через interval после конца предыдущей проверки. Если проверка заняла больше timeout, то тогда контейнер считается нездоровым. Если прошло больше N попыток, то также контейнер считается нездоровым.
В Dockerfile может быть лишь одна инструкция HEALTHCHECK, иначе только последняя будет выполняться. Это логично, так как Docker должен понимать какие проверки нужно проводить для определения состояния контейнера.
Есть два exit статуса команды — 0, если контейнер здоровый и готов к работе и 1 — если контейнер нездоров.
Изображение взято из документации Docker.
В этом примере каждые 5 минут проверяется ответ сервера в течение трех секунд.
SHELL
SHELL [«executable», «parameters»]
Инструкция SHELL используется для переопределения консоли по умолчанию для shell-формы инструкции CMD, ENTRYPOINT и RUN.
Эта инструкция полезная для Windows, где есть обычная консоль и powershell.
Изображение взято из документации Docker.
SHELL инструкция может быть написана несколько раз, таким образом заменяя прошлую. Это полезно, если нужно выполнить несколько команд на другой консоли, а затем вернуться к стандартной.
Примеры Dockerfile
Давайте рассмотрим несколько Dockerfile и разберемся в том, что там происходит.
FROM maven:3.8.5-openjdk-17 AS build
COPY /src /src
COPY pom.xml /
RUN mvn -f /pom.xml clean packageFROM openjdk:17-jdk-slim
COPY --from=build /target/*.jar application.jar
EXPOSE 8081
ENTRYPOINT [«java»,»-jar», «application.jar»]
В этом Dockerfile создается image в двух шагах. Первый шаг — на основе maven с openjdk-17 устанавливаются все зависимости и собирается Java-приложение.
Для этого в image:
копируется папка с исходным кодом приложения
копируется файл с конфигурацией Maven
выполняется команда mvn -f /pom.xml clean package, которая создает исполняемый jar-файл
На втором шаге на основе openjdk-17 создается image, который будет запускать Java-приложение.
Для этого в image:
из предыдущего шага копируется любой .jar файл в текущий image под именем application.jar
указывается информация о том, что приложение работает на порту 8081 (это только информация, не открывающая порты и не обязывающая приложение действительно работать на этом порту)
указывается точка входа в приложение — команда java -jar application.jar, запускающая Java-приложение из файла application.jar (когда запустится контейнер, а не во время сборки image)
Второй Dockerfile:
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY src ./src
COPY vocabularies ./vocabularies
COPY tsconfig.json ./
RUN npm run build
EXPOSE 3000
CMD [«npm», «start»]
На основе image node-18 создается контейнер Node.js приложения.
Для этого в image:
указывается рабочая директория как /app
копируются файлы package.json и package-lock.json (так как они подходят под паттерн package*.json)
выполняется команда npm install, которая устанавливает зависимости для приложения
копируется директория с исходным кодом, директория vocabularies, файл конфигурации TypeScript
выполняется команда npm run build, производящая компиляцию TypeScript в JavaScript
указывается информация о том, что приложение работает на порту 3000 (это только информация, не открывающая порты и не обязывающая приложение действительно работать на этом порту)
указывается команда для старта приложения — команда npm start (когда запустится контейнер, а не во время сборки image)
Dockerfile best practice
Есть несколько рекомендаций к написанию красивых и оптимизированных Dockerfile.
Старайтесь использовать официальные image в инструкции FROM. Официальные image — это те, у которых есть синяя галочка на DockerHub. Это безопасно.
Используйте alpine-версии. У множества image есть alpine-версии, которые весят меньше обычных.
Если указываете label, тогда предпочтите это делать в одной команде в одной строке, чтобы избежать создания дополнительных слоев.
Разделяйте большие и сложные RUN команды на несколько строк с помощью переноса строк. Так ваш Dockerfile будет более читаемым и поддерживаемым.
Используйте exec форму CMD — это форма вида CMD [«executable», «param1», «param2»]
Используйте принятые порты для своих приложений и указывайте их в EXPOSE инструкции. Так другие разработчики смогут понять какие порты контейнера можно использовать
Используйте переменные ENV, чтобы сделать запуск контейнера проще и более гибким.
RUN –mount=type=bind более эффективна для копирования, чем COPY. Но такие файлы добавляются только для выполнения инструкции. ADD стоит использовать, если вы хотите скачать файлы из удаленного пути или разархивировать архив.
Используйте VOLUME, чтобы указать Docker на необходимость сохранения определенной директории. Рекомендуется использовать VOLUME для всех данных, которые создаются пользователем.
Предпочтительно использовать инструкцию USER вместо sudo.
Используйте WORKDIR всегда, чтобы быть уверенным в правильности пути исполнения. Не используйте cd в RUN, так как это признак плохого кода.
Думайте об ONBUILD как об инструкции, которую дает родительский image дочернему. Если вы разрабатываете такие image, то используйте отдельный тег -onbuild, например ruby:1.9-onbuild