Как устроен Dockerfile

Поделимся советами как описать свой образ Docker.

Время чтения: 14 мин

Кратко

Скопировано

Dockerfile — это текстовый файл, в котором описан рецепт создания образа Docker. Рецепт состоит из инструкций, которые выполняются последовательно. Они содержат информацию об операционной системе, выбранной платформе, фреймворках, библиотеках, инструментах, которые нужно установить.

Dockerfile позволяет создавать одинаковое окружение для работы программы, независимо от машины, которая собирает образ. Прочитайте обзорную статью о Docker, чтобы лучше разобраться, зачем это нужно.

Как понять

Скопировано

Современные веб-приложения работают очень просто только для пользователя, который может смотреть видео, читать тексты, слушать музыку или заказывать еду «в один клик». Под капотом — это сложный набор приложений, взаимодействующих между собой. Если одно из приложений в цепочке сломается, клиент не сможет решить свою задачу. Важная задача веб-разработчика — сделать приложение максимально живучим в самых разных условиях. Решение задачи заключается в том, чтобы воспроизвести одинаковое, «правильное» поведение веб-приложения.

Docker умеет создать идентичные условия работы приложения, независимо от операционных систем и установленных служб на компьютере разработчика и на сервере. Для этого используется концепция контейнера. Прототипом контейнера является образ, в Dockerfile описывают процесс его создания в виде набора инструкций.

Инструкции записываются построчно. На первом месте указывается команда для Docker, которую нужно выполнить, а затем — список аргументов этой команды:

        
          
          # CommentINSTRUCTION arguments
          # Comment
INSTRUCTION arguments

        
        
          
        
      

Образ Docker легче всего представить в виде слоёного пирога или бургера: новый слой — новая инструкция. Образы для типичного веб-приложения могут быть собраны примерно так:

Примеры образов Docker для веб-приложений

Каждая новая инструкция — новый слой. В качестве инструкции можно выполнить команду в терминале, скопировать файлы внутрь образа или настроить связь с внешним миром с помощью сетевого окружения и томов. Docker объединяет файловые системы отдельных слоёв в одну во время сборки, используя механизм Union File Systems.

Концепция слоёв позволяет Docker оптимальным образом хранить данные на жёстком диске. Docker загружает только те слои, которых не было на компьютере прежде. При этом слой из одного образа может подойти и к другому.

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

        
          
          docker build
          docker build

        
        
          
        
      

Как пишется

Скопировано

Работу с Dockerfile можно разбить на два этапа: описание инструкций и сборка образа. Набор инструкций — последовательность действий, чтобы собрать образ.

Описание инструкций Dockerfile

Скопировано

FROM. Установка базового образа

Скопировано

Dockerfile обычно начинается с инструкции FROM. Эта инструкция задаёт базовый образ.

В качестве базового образа может быть использован образ с чистой операционной системой, образ с уже установленной и настроенной платформой или вообще любой другой образ. Вот так можно установить Ubuntu 18.04 как базовый образ:

        
          
          FROM ubuntu:18.04
          FROM ubuntu:18.04

        
        
          
        
      

Для веб-приложения на Node.js обычно используют официальный образ от команды Docker:

        
          
          FROM node
          FROM node

        
        
          
        
      

RUN. Запуск команд терминала

Скопировано

Инструкция RUN позволяет запускать команды терминала при сборке. Это самая используемая инструкция, ей можно создать папку, установить недостающие пакеты или запустить shell скрипт.

Например, установим платформу Node.js поверх образа с чистой Ubuntu:

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm

        
        
          
        
      

При сборке образа теперь будет произведена установка последней версии Node.js.

COPY и ADD. Копирование файлов проекта

Скопировано

Инструкции COPY и ADD позволяют перенести файлы с компьютера, который запускает сборку, внутрь образа.

Например, перенесём все содержимое папки, где лежит Dockerfile в папку /app внутри образа:

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npmCOPY . /app
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
COPY . /app

        
        
          
        
      

ENTRYPOINT и CMD. Запуск приложения

Скопировано

После того как образ готов, необходимо запустить приложение, которое в нем содержится. Образы Docker задумывались как упаковка для приложения, поэтому нет ничего удивительного в существовании механизма запуска приложения при старте контейнера на основе собранного образа. Для этого используют одну из двух инструкций: ENTRYPOINT и CMD.

Инструкция ENTRYPOINT используется для запуска приложения при старте контейнера:

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npmCOPY . /appENTRYPOINT ["node", "/app/app.js"]
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
COPY . /app
ENTRYPOINT ["node", "/app/app.js"]

        
        
          
        
      

В отличие от инструкции RUN эта инструкция получает полный доступ к инфраструктуре терминала на компьютере пользователя. Вместе с командой запуска контейнера вы можете передавать параметры команде, которая прописана после ENTRYPOINT или пользоваться системой сигналов Linux. Внутрь образа можно положить программу и запускать её внутри контейнера, передавая через параметры текстовые файлы со своего компьютера. Например, можно упаковать в контейнер утилиту для проверки орфографии yaspeller. В примере ниже она используется для проверки орфографии слов на русском и английском языках в файлах с расширением *.md и *.txt:

        
          
          FROM nodeRUN npm install yaspeller -gENTRYPOINT ["yaspeller"]
          FROM node
RUN npm install yaspeller -g
ENTRYPOINT ["yaspeller"]

        
        
          
        
      

Затем необходимо собрать образ, указав явно имя образа для удобства:

        
          
          docker build -t yaspeller .
          docker build -t yaspeller .

        
        
          
        
      

Запускать проверку орфографии в любой папке для файлов с расширением *.md и *.txt можно теперь простой командой:

        
          
          docker run --rm yaspeller .
          docker run --rm yaspeller .

        
        
          
        
      

Вместо . можно писать название файла или папки. Ключ --rm означает, что после завершения работы контейнер удалится из списка использованных Docker. Это важно, поскольку, пока контейнер хранится в этом списке, нельзя запустить контейнер с таким же именем, несмотря на то, что контейнер уже отработал и не используется.

Инструкция CMD делает практически то же самое. Обычно это также команда запуска приложения:

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npmCOPY . /appCMD ["node", "/app/app.js"]
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
COPY . /app
CMD ["node", "/app/app.js"]

        
        
          
        
      

CMD — инструкция запуска по умолчанию, она игнорируется в том случае, если пользователь вашего образа прописывает в явном виде, что и как запускать после запуска контейнера на основе образа. Обычно CMD вообще используется для передачи параметров по умолчанию вашему приложению, которые пользователь может переопределить.

В чем же разница между ENTRYPOINT и CMD? В ваших намерениях.

Есть две формы записи аргументов ENTRYPOINT и CMD: в виде строки и в виде массива строк. Первый вариант (так называемый shell режим) используется редко, поскольку не позволяет гибко настраивать работу образа. Обычно используется второй вариант (так называемый exec режим) — массив строк, который может состоять из команды и её параметров. Среди аргументов инструкции CMD строка с командой может и отсутствовать, если эта инструкция идёт после инструкции ENTRYPOINT. В этом случае строки массива рассматриваются как аргументы по умолчанию для команды, обозначенной в ENTRYPOINT.

ENV. Переменные окружения

Скопировано

Переменные окружения задаются инструкцией ENV.

Через переменные окружения передают ключи и пароли к сервисам, режим работы, другие секретные и не очень значения. Например, запуск приложения Node.js для конечного пользователя обозначается дополнительной инструкцией:

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npmCOPY . /appENV NODE_ENV=productionCMD ["node", "/app/app.js"]
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
COPY . /app
ENV NODE_ENV=production
CMD ["node", "/app/app.js"]

        
        
          
        
      

WORKDIR. Рабочая папка проекта

Скопировано

Инструкция WORKDIR задаёт рабочую папку приложения. Все инструкции в Dockerfile будут выполняться относительно неё.

Устанавливать рабочую папку — хороший тон. Она позволяет явно указать место, где будет происходить вся работа. Добавим её в нашу конфигурацию:

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npmWORKDIR /appCOPY . .ENV NODE_ENV=productionCMD ["node", "app.js"]
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
WORKDIR /app
COPY . .
ENV NODE_ENV=production
CMD ["node", "app.js"]

        
        
          
        
      

USER. Запуск от имени пользователя

Скопировано

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

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npmWORKDIR /appCOPY . .ENV NODE_ENV=productionUSER node_userCMD ["node", "app.js"]
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
WORKDIR /app
COPY . .
ENV NODE_ENV=production
USER node_user
CMD ["node", "app.js"]

        
        
          
        
      

EXPOSE. Проброска порта вовне

Скопировано

Для запуска веб-приложения на компьютере вы используете веб-сервер, запущенный локально. Обычно веб-приложение становится доступным по адресу http://localhost:8080. Цифры в конце означают порт, открытый для запросов со стороны браузера или других приложений. Чтобы открыть в браузере веб-приложение, запущенное внутри контейнера, нужно «пробросить» запросы от браузера внутрь контейнера, а ответ от веб-приложения из контейнера наружу. Для этого используется перенаправление пакетов в виртуальном сетевом окружении (Docker Network):

Проброска портов образов Docker для веб-приложений

EXPOSE незаменим, когда в образе находится база данных и нам нужен доступ к ней вне контейнера. Для этого используется инструкция EXPOSE:

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npmWORKDIR /appCOPY . .ENV NODE_ENV=productionUSER node_userEXPOSE 8080CMD ["node", "app.js"]
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
WORKDIR /app
COPY . .
ENV NODE_ENV=production
USER node_user
EXPOSE 8080
CMD ["node", "app.js"]

        
        
          
        
      

Запись EXPOSE 8080 означает, что на компьютере, на котором запущен Docker, веб-приложение будет доступно по адресу http://localhost:8080.

ARG. Аргументы командной строки

Скопировано

Во время сборки образа не всегда удобно, а иногда даже опасно, описывать все параметры внутри Dockerfile, поскольку этот файл обычно доступен в репозитории большинству разработчиков. В случае публичного репозитория это недопустимо вовсе. В этом случае следует пользоваться переменными, значения которых задаются на этапе сборки образа.

Передавать данные можно с помощью аргументов команды docker build на этапе сборки образа. Во время сборки эти аргументы можно использовать как переменные, достаточно их определить инструкцией ARG. Можно задать и значения по умолчанию на тот случай, если пользователь не укажет нужные аргументы. Например, передать имя пользователя внутрь контейнера можно следующим образом:

        
          
          docker build --build-arg user=node_user .
          docker build --build-arg user=node_user .

        
        
          
        
      

В Dockerfile надо будет добавить соответствующие инструкции:

        
          
          FROM ubuntu:18.04RUN sudo apt update && sudo apt install nodejs && sudo apt install npmWORKDIR /appCOPY . .ENV NODE_ENV=production# Значение по умолчанию 'deploy' (можно не указывать)ARG user=deployUSER $userEXPOSE 8080CMD ["node", "app.js"]
          FROM ubuntu:18.04
RUN sudo apt update && sudo apt install nodejs && sudo apt install npm
WORKDIR /app
COPY . .
ENV NODE_ENV=production
# Значение по умолчанию 'deploy' (можно не указывать)
ARG user=deploy
USER $user
EXPOSE 8080
CMD ["node", "app.js"]

        
        
          
        
      

Важно, что так не следует передавать секретные данные, поскольку их можно будет увидеть в истории Docker:

        
          
          docker history
          docker history

        
        
          
        
      

Для безопасной передачи секретных данных лучше использовать тома Docker.

Многоступенчатая сборка образа

Скопировано

С точки зрения оптимизации сборки, уменьшения размера образа и ускорения приложения, образ можно собирать в несколько этапов. Например, с помощью платформы Node.js произвести сборку веб-приложения на первом этапе, а на втором — запустить готовый бандл с помощью веб-сервера. Операция копирования из первого промежуточного образа во второй целевой пройдёт совершенно незаметно. После сборки образ будет занимать мало дискового пространства, в нем будет все самое необходимое для работы веб-приложения:

        
          
          # Сборка проекта на платформе Node.jsFROM node:lts-alpine as build-stageWORKDIR /appCOPY package*.json ./RUN npm installCOPY . .RUN npm run build# Запуск приложения на сервереFROM nginx:stable-alpine as production-stageCOPY --from=build-stage /app/dist /usr/share/nginx/htmlEXPOSE 80CMD ["nginx", "-g", "daemon off;"]
          # Сборка проекта на платформе Node.js
FROM node:lts-alpine as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# Запуск приложения на сервере
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

        
        
          
        
      

Имя промежуточного образа build-stage служит для передачи результата работы первой стадии сборки.

Рекомендации

Скопировано

Для того чтобы использовать образы эффективнее, необходимо следовать рекомендациям от команды Docker:

  1. Нужно создавать образы так, чтобы жизненным циклом контейнера можно было удобно управлять. Образ не должен хранить внутреннее состояние. Данные внутрь образа можно передать на этапе сборки с помощью аргументов командной строки, а на этапе работы контейнера можно пользоваться томами Docker.
  2. Необходимо понимать контекст запуска веб-приложения: папка проекта, удалённый ресурс (remote source) или репозиторий.
  3. Надо понимать, что Dockerfile может запускаться вне контекста через стандартный поток ввода.
  4. Используйте файл .dockerignore для того, чтобы в образ попадали только нужные файлы и папки. От всего лишнего лучше избавиться на этапе сборки.
  5. Используйте сборку приложения в несколько стадий. Это позволит существенно уменьшить размер образа.
  6. Не устанавливайте то, что не будете использовать в образе.
  7. Необходимо разделять приложения на обособленные части, которые способны выполняться независимо. Этот процесс носит название декаплинга (Decoupling).
  8. Минимизируйте количество слоёв в образе. Это повышает производительность образа как при сборке, так и при работе контейнера.
  9. Если параметры инструкции записываются в несколько строк (с помощью символа переноса строки \) необходимо выстраивать аргументы в алфавитном порядке. Это повышает читаемость файла и упрощает отладку.
  10. Используйте кэш Docker только для тех слоёв, которые будут нужны для сборки других образов. Для этого достаточно добавить параметр --no-cache=true в команду сборки docker build.

Сборка образа

Скопировано

Образ Docker можно собрать тремя способами:

– указав путь к папке PATH;
– указав путь к репозиторию URL;
– используя стандартный поток ввода .

Чаще всего используется первый способ с указанием пути. Самая простая команда для сборки образа:

        
          
          docker build .
          docker build .

        
        
          
        
      

С помощью этой команды собираться образ будет из текущей папки (. в конце), в которой должен быть Dockerfile.

Использование нескольких Dockerfile

Скопировано

Иногда возникает необходимость использования нескольких вариантов сборок в одном проекте. В этом случае не обойтись без нескольких файлов с инструкциями. При сборке можно указать другое имя для файла конфигурации или относительный путь внутри PATH, нужно использовать флаг -f:

        
          
          docker build -f containers/dockerfile-mode-1 .
          docker build -f containers/dockerfile-mode-1 .

        
        
          
        
      

Точно так же можно указать относительный путь для проекта или репозитория по некоторому URL. Например, Docker может скачать не только репозиторий GitHub, но и произвольный архив с проектом, распаковать его и собрать образ:

        
          
          docker build -f ctx/Dockerfile http://server/ctx.tar.gz
          docker build -f ctx/Dockerfile http://server/ctx.tar.gz

        
        
          
        
      

Поддерживаются архивы форматов bzip2, gzip, xz.

Файлы и папки проекта, исполняемый файл приложения, архив или репозиторий Git составляют контекст образа. Но Docker позволяет собирать образы без контекста из стандартного потока ввода. Собрать такой образ можно командой:

        
          
          docker build - < Dockerfile
          docker build - < Dockerfile

        
        
          
        
      

Исключение файлов из сборки .dockerignore

Скопировано

Если вам не нужно включать в образ какие-то папки или файлы из контекста, добавьте в папку файл исключений .dockerignore. В этом файле перечисляются в отдельных строках все пути или маски путей, которые не должны быть помещены в образ. Пример файла:

        
          
          # Комментарий*/temp**/*/temp*temp?
          # Комментарий
*/temp*
*/*/temp*
temp?

        
        
          
        
      

*/temp позволяет не включать в образ файлы или папки, имена которых начинаются на temp, и которые находятся в любой папке первого уровня (например, /somedir/temporary.txt или /somedir/temp);
*/*/temp* — делает то же, но для папок второго уровня;
temp? — позволяет не включать в образ файлы и папки из корневой папки образа, имена которых начинаются на temp и состоят из пяти символов, последний из которых может быть любым.

На практике

Скопировано

Игорь Коровченко советует

Скопировано

🛠 При сборке образа Docker ищет файл с исключениями, исходя из имени основного файла конфигурации. Например, команда docker build -f myapp.Dockerfile . будет в первую очередь искать файл исключений по пути myapp.Dockerfile.dockerignore. Если такого файла не найдётся, то при наличии будет использоваться .dockerignore.

🛠 Все объекты Docker обязательно имеют имя. Если оно не назначается пользователем, то в качестве имени используется хэш. Чтобы работать с образами было удобнее, лучше выбирать имя. Кроме имени можно также использовать теги. Обычно теги указывают на версию или особенности той или иной сборки образа. Совокупность образов с одним именем, но разными тегами, — это репозиторий образов. Например, можно собрать образ с указанием имени и тега командой:

        
          
          docker build -t vieux/apache:2.0 .
          docker build -t vieux/apache:2.0 .

        
        
          
        
      

Имя образа vieux/apache использует принятое в терминологии Docker соглашение. До слэша / указывается имя пользователя, после — имя образа или репозитория образов. В качестве имени пользователя чаще всего подразумевается имя пользователя в реестре репозиториев. Исторически первым и самым крупным является официальный реестр компании Docker — Docker Hub.

После двоеточия : можно указать тег. В примере этот тег обозначает версию сборки, но теги также могут указывать и на вариант образа, например, на основе Alpine Linux или Ubuntu. Вы можете указать с помощью тега, что версия базового образа должна быть последней, и в момент сборки именно она попадёт в образ.

🛠 Важно отслеживать размер образа, и пользоваться образами с умом. Вы можете загрузить к себе образ командой pull. Например, для образа Node.js:

        
          
          docker pull node
          docker pull node

        
        
          
        
      

Уже на этапе клонирования образа вы увидите его размер. После скачивания и установки также можно получить информацию об образе с помощью команды:

        
          
          docker image inspect node
          docker image inspect node

        
        
          
        
      

В терминале будет выведена информация об образе в формате JSON. По ключу "Size" можно увидеть размер образа в байтах. Большое внимание в технологии Docker уделяется скорости сборки и размеру образов. Именно поэтому рекомендуется использовать наименьший образ операционной системы, который обеспечивает весь набор инструментов и служб, необходимый для запускаемого приложения. Практически стандартом стало использование Alpine Linux в качестве базовой операционной системы. В официальном репозитории образов Node.js есть образ и на основе этой операционной системы: node:lts-alpine.

На собеседовании

Скопировано
Задать вопрос в рубрику
🤚 Я знаю ответ

Редакция
Игорь Коровченко  отвечает

Скопировано

Обычно жизненный цикл контейнера состоит из следующей последовательности состояний:

  1. Создание контейнера
  2. Работа контейнера
  3. Приостановка контейнера
  4. Возобновление работы контейнера
  5. Запуск контейнера
  6. Остановка контейнера
  7. Перезапуск контейнера
  8. Принудительная остановка контейнера
  9. Удаление контейнера

На практике встречаются и более сложные случаи, поэтому жизненный цикл контейнера лучше представить с помощью диаграммы:

Схема жизненного цикла контейнера Docker с указанием команд управления

На этой диаграмме показаны не только состояния и пути перехода из одного состояния в другое, но и команды, которые позволяют пользователю их менять.