Эта публикация - текстовый вариант и сценарий для видео на 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=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
Image должен быть валидным и он может спулиться с DockerHub или другого публичного репозитория во время сборки.
FROM
может использоваться несколько раз, если сборка представляет многошаговый процесс, тогда каждому шагу можно дать имя с помощью AS name
в инструкции FROM
.
Можно воспользоваться COPY –from=<name>
и обратиться к предыдущему собранному image.
Tag или digest необязательны, если вы не указываете их, тогда Docker пытается найти тег latest
и выбрасывает ошибку если предоставленный тег не найден.
Может быть применен дополнительный тег –platform
, чтобы указать платформу image, например linux/amd64
или windows/amd64
. По умолчанию используется платформа, на которой собирается image.
Комментарии
Docker считает строки, которые начинаются с # как комментарии.
В другие местах # считается аргументом. То есть вы не можете написать комментарий с середины строки. Комментарии однострочные, и если хотите продолжить, то нужно указать решетку еще раз.
Перед исполнением Dockerfile комментарии удаляются, поэтому они никак не влияют на построение image.
Табуляция и пробелы перед инструкциями и комментариями допускаются, но не рекомендуются.
Эти примеры будет работать одинаково.
# это комментарий
RUN echo hello
RUN echo world
# это комментарий
RUN echo hello
RUN echo world
Директивы парсера
Директивы парсера не обязательны. Директивы не добавляют слои в image. Директивы представляются как комментарий, который выглядит как ключ=значение
. Одна директива может использоваться лишь раз. Она не чувствительна к регистру и может иметь пробелы и табуляцию между знаком решетки и названием директивы.
После первой встречной инструкции или комментария или пустой строки директивы перестают быть директивами и становятся комментариями. Поэтому они должны быть написаны подряд в начале Dockerfile без прерываний.
Директивы из этих примеров не будут работать из-за нарушения правил.
Она не может быть разделена на две строки, не может повторяться, должна находиться до первой инструкции и комментария. Неизвестная директива воспринимается как комментарий.
Вот примеры невалидных директив.
В этом случае строка разрывается, что делает директиву невалидной.
#direc \
tive=value
В этом случае одна и та же директива дублируется.
#directive=value1
#directive=value2
FROM SomeImageName
В этом случае директива воспринимается как комментарий, потому что идет после инструкции FROM.
FROM SomeImageName
#directive=value
А эта директива невалидная, потому что идет после комментария.
# какой-то коментарий
#directive=value
FROM SomeImageName
Если директива неизвестна, то она воспринимается как комментарий, что делает все остальные директивы после нее невалидными.
#unknowndirective=value
#knowndirective=value
Есть две директивы парсера - syntax
и escape
.
Syntax используется при сборке с помощью BuildKit, это не обычный клиент, поэтому я не буду рассказывать про нее.
Escape обозначает символ конца строки в Dockerfile. По умолчанию это \
.
Обычно в качестве разделителя на Windows используется апостроф `
, где \
является разделителем директорий.
Аргументы
Мы помним, что аргументы могут быть указаны до инструкции FROM
, они также могут быть указаны и после инструкции FROM
.
Инструкция аргумента выглядит как:
ARG <name>[=<default value>]
Значение по умолчанию можно опустить.
Эта инструкция определяет те аргументы, которые пользователь может передать в команду build
, во время сборки image при помощи флага --build-arg <varname>=<value>
. Если аргумент не передан, тогда используется дефолтный.
Например, вы можете передать версию базового image или название директории, в которой будет происходить какая-нибудь логика.
Значение аргумента и переменной можно получить при помощи знака доллара с названием переменной, либо же обернутого в фигурные скобки названия переменной. Поддерживаются модификаторы, которые позволяют менять значение.
Например, если вы указываете ${argname:-word}
, то если аргумент argname
был передан, его значение будет использовано, иначе слово word.
Если вы указываете ${argname:+word}
, тогда если аргумент был передан, будет использоваться слово word
, иначе пустая строка.
Это же работает и с переменными. Переменные можно получать на основе других переменных и аргументов с помощью обращения по названию.
Аргумент доступен в инструкциях после его упоминания. То есть в данном примере во второй строке user
будет иметь значение some_user
, потому что аргумент не определен еще на тот момент, а в четвертой уже переданный username
.
FROM busybox
USER ${username:-some_user}
ARG username
USER $username
Также если вы хотите использовать один и тот же аргумент в нескольких стадиях сборки, то необходимо в каждой из них определять аргумент.
FROM busybox
ARG SETTINGS
RUN ./run/setup $SETTINGS
FROM busybox
ARG SETTINGS
RUN ./run/other $SETTINGS
Не передавайте секреты и пароли с помощью аргументов, потому что они будут видны в docker history
. Для этого нужно использовать инструкцию RUN –mount=type=secret
. Об этом я расскажу позже.
Переменные
Инструкция выглядит следующим образом:
ENV <key>=<value> ...
При вызове docker build
или docker run
можно переопределить переменные с помощью флага –env
.
По сути эта инструкция работает как и аргументы. Но есть несколько отличий.
Вы можете указывать несколько переменных в одной строке. Если значение включает пробелы, то нужно обернуть его в двойные кавычки.
В отличие от аргументов, переменные хранятся в image и доступны для просмотра с помощью
docker inspect
или в Docker Desktop.Переменные наследуются из родительского image.
Значения переменных доступны после конца инструкции, то есть в таком случае значение def
будет равно hello
, а ghi
будет равно bye
.
ENV abc=hello
ENV abc=bye def=$abc
ENV ghi=$abc
Если хотите, чтобы значение переменной использовалось лишь во время сборки, тогда установите значение переменной при вызове команды RUN
. Либо же воспользуйтесь инструкцией ARG
.
RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ...
WORKDIR
Инструкция WORKDIR
устанавливает рабочую директорию для выполнения следующих команд.
WORKDIR /path/to/workdir
По умолчанию рабочая директория это корень файловой системы. Вам может понадобиться конкретная папка, поэтому можно использовать WORKDIR
.
ENV DIRPATH=/path
WORKDIR $DIRPATH/$DIRNAME
RUN pwd
Эта инструкция может быть вызвана несколько раз. Если в начале пути стоит /
, тогда путь к рабочей директории будет абсолютный, а если /
нет, то относительный текущего.
Тут работает подстановка аргументов и переменных, поэтому в данном случае рабочая директория будет path/$DIRNAME
Рабочая директория наследуется от базового image, поэтому рекомендуется устанавливать ее явно при описании своего Dockerfile.
RUN
Инструкция RUN
имеет два вида:
RUN <command>, выполняется в консоли /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=<TYPE>]
Этот флаг позволяет создать файловую систему, которая будет доступна во время сборки image.
Это может понадобиться для доступа к файлам на хосте, обращению к секретам или использования кэша для ускорения сборки.
Типы mount:
Bind
- default, readonlyCache
- временная директория для кэша для компиляторов и пакетных менеджеровSecret
- позволяет Docker получить доступ к секретным файлам без копирования их в imageSsh
- позволяет Docker получить доступ к SSH ключам через SSH агентов
CMD
У инструкции CMD
есть три формы:
CMD ["executable", "param1", "param2"] - exec-форма, она предпочтительна
CMD ["param1", "param2"] - по умолчанию для ENTRYPOINT
CMD command param1 param2 - shell-форма
В Dockerfile может быть лишь одна инструкция CMD
, иначе только последняя будет выполнена.
Основная задача инструкции CMD
- предоставить действие по умолчанию для исполняющегося контейнера. Она может начинаться с выполняемой команды, а может и не начинаться, но тогда нужно указать инструкцию ENTRYPOINT
и обе инструкции должны быть в JSON формате - с двойными кавычками.
Как и для RUN
, exec форма не вызывает командную строку, а shell форма вызывает. Поэтому если хотите использовать консоль, то либо используйте shell форму, либо передавайте явно консоль.
Если вы передадите аргументы в docker run
, тогда они переопределят те аргументы из инструкции CMD
.
Разница между RUN
и CMD
в том, что RUN
выполняется во время сборки и создает новый слой в image, а CMD
не выполняется во время сборки, но исполняется при запуске контейнера.
ENTRYPOINT
Давайте рассмотрим инструкцию ENTRYPOINT
, которая используется в паре с CMD
.
У нее есть две формы:
ENTRYPOINT [“executable”, “param1”, “param2”] - exec-форма предпочитаемая
ENTRYPOINT command param1 param2 - shell-форма
ENTRYPOINT
позволяет вам сконфигурировать контейнер, который будет работать как исполняемый, то есть указать команду, которая выполнится при старте контейнера.
Можно переопределить ENTRYPOINT
, если вызвать docker run –entrypoint
Как и CMD
только последняя инструкция ENTRYPOINT
будет исполнена
Есть несколько правил работы CMD
и ENTRYPOINT
:
В Dockerfile должен быть определен как минимум
CMD
илиENTRYPOINT
или обе инструкцииENTRYPOINT
должна быть использована когда контейнер используется как исполняемое приложениеCMD
должна быть использована для определения аругментов по умолчанию дляENTRYPOINT
CMD
будет перезаписан, когда контейнер будет запущен с другими аргументами
Если CMD
был определен в базовом image, то он не наследуется, поэтому его надо будет переопределять в текущем image.
LABEL
LABEL <key>=<value> <key>=<value> <key>=<value> ...
Инструкция LABEL
добавляет метаданные в image, это ключ-значение. Тут например, можно хранить информацию о версии приложения, каких-либо других параметрах.
Можно переносить аргументы на новые строки, либо же писать на одной либо же писать несколько инструкций.
Необходимо использовать двойные кавычки а не одиночные, иначе не вызовется подстановка переменных.
LABEL example="foo-$ENV_VAR"
Лейблы с базовых image наследуются и переопределяются, если указан тот же ключ.
Чтобы увидеть все лейблы image, используйте docker image inspect
.
EXPOSE
EXPOSE <port> [<port>/<protocol>...]
Инструкция EXPOSE
информирует Docker о том, что контейнер слушает определенный порт, когда он запущен. Можно указать TCP либо UDP соединение, по умолчанию TCP.
EXPOSE
не открывает порт наружу на самом деле. Он лишь информирует пользователя image о работающих портах. Чтобы получить доступ к порту контейнера нужно явно указать флаг -p
при создании контейнера.
Если контейнеры работают в одной сети, то они могут обращаться друг к другу без раскрытия порта хосту, что делает работу сети контейнеров более безопасной.
Вы можете создать контейнер базы и контейнер приложения, разместить их в одной сети, и приложение сможет обращаться к базе, в то время как снаружи Docker вы не сможете попадать в базу без явного раскрытия портов.
ADD
ADD [--chown=<user>:<group>] [--chmod=<perms>] [--checksum=<checksum>] <src>... <dest>
ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
У инструкции ADD
есть две формы. Она копирует файлы, директории и удаленные URLs, и добавляет их в файловую систему image.
Можно использовать wildcard из Golang filepath. Match, чтобы копировать несколько файлов, подходящих под этот паттерн.
ADD hom* /mydir/
ADD hom?.txt /mydir/
Указав относительный путь, файл будет добавлен в WORKDIR/relativeDir
.
ADD test.txt relativeDir/
Указав абсолютный путь, файл будет добавлен в корень.
ADD test.txt /absoluteDir/
Есть несколько правил:
<src> должен быть внутри build context, нельзя добавить файлы из вне контекста
Директория не копируется, копируется только содержимое
Если <src> это директория, то все ее содержимое копируется
Если <src> это архив, тогда он распаковывается
Если <dest> не существует, то он создается со всеми нужными путями для него
С помощью этой инструкции можно провалидировать хэшсумму файла.
COPY
COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
Инструкция COPY
копирует файлы и директории из <src> и добавляет их в файловую систему image
Как и в ADD
поддерживаются wildcard, логика абсолютных и относительных путей. Эти инструкции схожи между собой, но есть несколько отличий.
ADD
поддерживает URLADD
автоматически извлекает локальные tar-архивыCOPY
принимает только локальные файлы
Рекомендуется использовать COPY
, так как ADD
предоставляет дополнительные функции, которые следует использовать с осторожностью.
VOLUME
VOLUME ["/data"]
VOLUME
используются для того, чтобы разделять одну директорию между хостом, на котором работает Docker и контейнером. Так, например, можно сохранять данные базы данных и они не будут очищаться при перезапуске контейнера.
С помощью этой инструкции можно сказать Docker о том, что необходимо сохранить некоторую директорию с вложенными файлами. И тогда при старте контейнера будет создан volume, соответствующий этой директории.
Несколько особенностей volumes:
При работе на Windows, volume должен быть несуществующей или пустой директорией и это не может быть диск С
Если на каких-нибудь шагах построения image содержимое volume меняется, то эти изменения не будут сохранены
Нужно использовать двойные кавычки как и везде, так как аргумент инструкции - это массив JSON строк
Директория на хосте, которая будет хранить данные volume зависит от самого хоста и определяется на момент создания и старта контейнера.
USER
USER <user>[:<group>]
USER UID[:GID]
Инструкция USER
необходима для установки имени пользователя и группы или их id для использования по умолчанию. Этот user доступен в инструкциях RUN
, ENTRYPOINT
и CMD
.
Если у юзера нет определенной группы, то он будет в группе root
.
FROM microsoft/windowsservercore
# создается Windows пользователь внутри контейнера
RUN net user /add patrick
# этот пользователь устанавливается для выполнения следующих комманд
USER patrick
В этом примере создается новый пользователь Патрик и он в дальнейшем используется в ходе построения 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, таким образом они не наследуются дальше первого уровня.
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
Например, мы делаем свой 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 - если контейнер нездоров.
HEALTHCHECK --interval=5m --timeout=3s \
CMD curl -f http://localhost/ || exit 1
В этом примере каждые 5 минут проверяется ответ сервера в течение трех секунд.
SHELL
SHELL ["executable", "parameters"]
Инструкция SHELL
используется для переопределения консоли по умолчанию для shell-формы инструкции CMD
, ENTRYPOINT
и RUN
.
Эта инструкция полезная для Windows, где есть обычная консоль и powershell.
FROM microsoft/windowsservercore
# выполняется как cmd /S /C echo default
RUN echo default
# выполняется как cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default
# выполняется как powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello
# выполняется как cmd /S /C echo hello
SHELL ["cmd", "/S", "/C"]
RUN echo hello
SHELL
инструкция может быть написана несколько раз, таким образом заменяя прошлую. Это полезно, если нужно выполнить несколько команд на другой консоли, а затем вернуться к стандартной.
Пример Dockerfile
Давайте рассмотрим Dockerfile и разберемся в том, что там происходит.
FROM maven:3.8.5-openjdk-17 AS build
COPY /src /src
COPY pom.xml /
RUN mvn -f /pom.xml clean package
FROM 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 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