На 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:20
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. Для этого понадобится подготовительная работа:
- Регистрация https://hub.docker.com/
- Подключение к аккаунту через запуск команды
docker login
в терминале. Docker попросит ввести имя пользователя и пароль - Создание репозитория с именем 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
для вывода части приветствия на страницу.
- Повторите все шаги из урока
- Создайте свой собственный образ на основе devops-example-app, в котором уже будет указана эта переменная окружения
- Проверьте, что приложение работает. Для этого откройте страницу http://localhost:3000. Вы должны увидеть на экране "Привет от Хекслета! Приложение запущено и передает сообщение: [значение из переменной SERVER_MESSAGE]"
- Опубликуйте образ на dockerhub
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.