Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Подготовка собственного образа Docker: Основы

На Docker Hub выложено множество готовых образов, которые используются администраторами и разработчиками: интерпретаторы и компиляторы языков, веб-сервера, базы данных и многое другое. Большую часть из них можно использовать на серверах без изменений, передав какие-то переменные окружения. Но для любого разрабатываемого приложения нужно создавать свой собственный образ. В него войдет код приложения и все его зависимости. Даже когда нам будет нужно изменить всего лишь конфигурацию, например Nginx, все равно придется создать свой собственный образ, в который добавлен конфигурационный файл.

В этом уроке мы научимся создавать Docker-образ на примере JavaScript проекта: данный язык программирования достаточно распространен в среде разработчиков. Но все описанные принципы так же будут подходит и для других языков. Для создания образа будем использовать популярный микрофреймворк fastify.

Для начала создадим каркас приложения с помощью готового шаблона:

cd /var/tmp # можно выбрать любую директорию
mkdir docker-fastify-example
cd docker-fastify-example
docker run --user $(id -u) -it -w /out -v `pwd`:/out node npm init fastify

Need to install the following packages:
  create-fastify
Ok to proceed? (y) y # введите y
generated .gitignore
generated README.md
generated app.js
generated .vscode/launch.json
generated plugins/README.md
generated routes/root.js
generated test/helper.js
generated plugins/sensible.js
generated plugins/support.js
generated routes/README.md
generated routes/example/index.js
generated test/routes/root.test.js
generated test/plugins/support.test.js
generated test/routes/example.test.js
--> project example generated successfully
run 'npm install' to install the dependencies
run 'npm start' to start the application
run 'npm run dev' to start the application with pino-colada pretty logging (not suitable for production)
run 'npm test' to execute the unit tests

Эта команда создаст шаблон приложения в директории /out запущенного контейнера, которая, на самом деле, является директорией /var/tmp/docker-fastify-example на нашей машине. В итоге у нас получается такая структура проекта:

. # docker-fastify-example
├── README.md
├── app.js
├── package.json
├── plugins
├── routes
└── test

Для запуска этого приложения, нам нужно выполнить две основные задачи: установить зависимости и запустить сервер. Без Docker это выглядит так:

# Если не стоит npm,
# то сюда еще входит установка Node.js
npm install
npm start # или npm run dev в режиме разработки

Установку зависимостей нужно выполнить еще до создания образа, так как во время первой установки формируется файл package-lock.json. Он нужен для фиксации зависимостей: с его помощью мы гарантируем, что в образе будут использоваться ровно те зависимости, которые мы подключали во время разработки. Сделать это можно следующим образом:

# внутри директории docker-fastify-example
docker run -it -w /out -v `pwd`:/out node npm install

added 398 packages, and audited 560 packages in 45s

Теперь директория с приложением выглядит так:

.
├── README.md
├── app.js
├── node_modules # тут хранятся зависимости
├── package-lock.json # новый файл
├── package.json
├── plugins
├── routes
└── test

Сборка и публикация Docker-образа

Docker создает образ на основе файла Dockerfile, в котором описываются необходимые команды. Мы начнем сразу с примера:

FROM node:18

WORKDIR /app

COPY package.json .
COPY package-lock.json .

RUN npm ci

COPY . .

ENV FASTIFY_ADDRESS 0.0.0.0

# Команда, которая запускается автоматически
# при старте контейнера
CMD ["npm", "start"]

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

Для сборки образа в директории с Dockerfile нужно выполнить команду указанную ниже:

# -t, --tag - имя образа и тега. По умолчанию latest
# Точка в конце важна, подробнее про нее дальше
docker build -t hexlet/docker-fastify-example .

[+] Building 26.4s (12/12) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 190B
 => [internal] load .dockerignore
 => => transferring context: 2B
 => [internal] load metadata for docker.io/library/node:18
 => [auth] library/node:pull token for registry-1.docker.io
 => [internal] load build context
 => => transferring context: 63.29MB
 => [1/6] FROM docker.io/library/node:18@sha256:e5b7b3
 => [2/6] WORKDIR /app
 => [3/6] COPY package.json .
 => [4/6] COPY package-lock.json .
 => [5/6] RUN npm ci
 => [6/6] COPY . .
 => exporting to image
 => => exporting layers
 => => writing image sha256:52f6fe
 => => naming to docker.io/library/docker-fastify-example

Сборка образа занимает какое-то время: нужно подождать, пока выполнятся все команды. Как результат, в списке образов появляется образ с именем hexlet/docker-fastify-example и тегом latest. Его можно запустить и убедиться в работоспособности:

# По умолчанию Fastify стартует на 3000 порту
# Docker запускает команду npm start
docker run -it -p 3000:3000 hexlet/docker-fastify-example

{"level":30,"time":1651503036761,"pid":22,"hostname":"a9b1ea7fc320","msg":"Server listening at http://0.0.0.0:3000"}

Для полной проверки, откройте в браузере ссылку http://localhost:3000 и убедитесь что сайт открылся. Остался последний шаг — загрузить образ на Docker Hub. Для этого понадобится подготовительная работа:

  1. Регистрация https://hub.docker.com/
  2. Подключение к аккаунту через запуск команды docker login в терминале. Docker попросит ввести имя пользователя и пароль
  3. Создание репозитория с именем docker-fastify-example в личном кабинете

Теперь, чтобы загрузить образ в Docker Hub, мы должны дать ему правильное имя. По соглашению, часть имени Docker-образа до символа /, должна совпадать с именем вашего пользователя Docker Hub. Чтобы так сделать, вам необходимо запустить команду сборки еще раз:

docker build -t <имя вашего пользователя>/docker-fastify-example .

Теперь можно пушить:

docker build -t <имя вашего пользователя>/docker-fastify-example .
# По умолчанию отправляется тег latest
docker push <имя вашего пользователя>/docker-fastify-example

Теги

Теги у Docker-репозиториев изменяемые. Если изменить образ и снова его запушить с тем же тегом, образ поменяется. Для тега latest это ожидаемое поведение, а вот для версий нет. За этим нужно следить самостоятельно и не менять образ для уже существующих тегов. Если меняется образ, то правильно создавать новый тег:

# Используем тег
docker build -t <имя вашего пользователя>/docker-fastify-example:v2 .
docker push <имя вашего пользователя>/docker-fastify-example:v2

Команды Dockerfile

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

FROM

# Варианты

# По умолчанию тег latest
FROM ubuntu

# С явно указанным тегом
FROM node:18

Образ — это в первую очередь файловая система, которая формируется на базе команд описанных в Dockerfile. Docker берет какую-то первоначальную файловую систему и затем изменяет ее в соответствии с описанием. Получившаяся структура файлов и становится образом. Откуда берется первоначальная файловая система?

Практически все образы в Docker формируются не с нуля, а на базе уже существующих образов. Образы формируют дерево, в котором одни образы наследуют файловые системы других образов начиная с базового образа scratch.

# Иерархия образов
docker-fastify-example
  FROM node
    FROM buildpack-deps:bullseye
      FROM buildpack-deps:bullseye-scm
        FROM buildpack-deps:bullseye-curl
      FROM debian:bullseye
        FROM scratch

Команда FROM задает образ, чья файловая система берется за основу. Все последующие команды, которые изменяют файловую систему, работают уже с ней. Потому команда FROM идет первой в Dockerfile.

WORKDIR

WORKDIR /app

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

docker run -it hexlet/devops-fastify-app bash
root@02d29c66ea06:/app# # мы оказались внутри /app

WORKDIR автоматически создает директорию, если ее еще нет.

COPY

# файлы

COPY package.json .
# Аналогично
# COPY package.json package.json

COPY package-lock.json .

# Копирование всех файлов внутрь
COPY . .

Команда COPY копирует файлы и директории с хост-машины внутрь Docker-образа. Она принимает два параметра: первый — что копируем, второй — куда копируем и под каким именем. Второй параметр может принимать три варианта:

  • Абсолютный путь, копирование происходит ровно по нему
  • Относительный путь, копирование происходит относительно установленной рабочей директории WORKDIR
  • Точка, файл или директория копируется как есть в рабочую директорию

Если точка идет первым параметром, то это обозначает что копироваться будет директория целиком.

Для полного понимания принципов работы команды COPY, нужно представлять что такое контекст. Помните, когда мы указывали точку во время сборки образа? Это и есть контекст:

docker build -t hexlet/docker-fastify-example .

Контекст — это директория, относительно которой работает первый параметр в COPY. Обычно контекстом указывают ту директорию, которая содержит Dockerfile. Но это не обязательно, ведь контекстом может быть и другая директория:

# Указана директория уровнем выше
# Dockerfile должен лежать в текущей директории, из которой идет запуск
docker build -t something ..

Во время сборки образа, контекст целиком копируется внутрь системных директорий Docker, из которых в образ переносится все, что указано в команде COPY. Из-за этого иногда возникают проблемы. Контекст может содержать директории, которые не должны попадать в образ, например, .git, или зависимости установленные локально (node_modules), так как они все равно устанавливаются заново во время сборки. Чтобы избежать их попадания во внутрь, нужно создать файл .dockerignore и указать там те директории и файлы, которые не должны быть частью контекста. Принцип работы файла такой же, как и у .gitignore.

node_modules
.git
logs
tmp

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

RUN

# Если базовый образ Ubuntu, то доступен apt
RUN apt-get update && apt-get install -q curl

RUN npm install

Команда RUN выполняет переданную строчку в терминале от пользователя root. С ее помощью вносятся основные изменения в файловую систему, добавляются пакеты, ставятся зависимости и так далее. Команд RUN может быть добавлено любое количество, обычно делают по одной команде на одно действие.

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

# -q - ставить автоматически не задавая вопросов
RUN apt-get install -q curl

CMD

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

# Используется CMD
docker run -it hexlet/docker-fastify-example # npm start
# CMD не используется, так как явно указан bash
docker run -it hexlet/docker-fastify-example bash

ENV

ENV FASTIFY_ADDRESS 0.0.0.0
ENV VERSION 1

Задает переменные окружения. Команды, выполняющиеся после ENV, видят эти переменные и могут их использовать.

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

docker run -it -p 3000:3000 -e NODE_ENV=production hexlet/docker-fastify-example

Самостоятельная работа

Приложение devops-example-app использует переменную окружения SERVER_MESSAGE для вывода части приветствия на страницу.

  1. Создайте свой собственный образ на основе devops-example-app, в котором уже будет указана эта переменная окружения
  2. Проверьте, что приложение работает
  3. Опубликуйте образ на dockerhub

Дополнительные материалы

  1. Docker Documentation: Dockerfile reference

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и соглашаетесь с «Условиями использования»