В сегодняшних реалиях большинство компаний активно используют систему контейнеризации Docker в своих проектах, особенно при работе с микросервисными приложениями. С помощью Docker можно быстро развертывать любые приложения, как монолитные, так и предназначенные для работы в облачных технологиях. Несмотря на простоту работы с Docker, необходимо знать некоторые особенности создания своих собственных образов. В сегодняшней статье мы рассмотрим, как работать с образами Docker, а также оптимизировать их, на примере двух различных приложений.
Для работы с системой контейнеризации Docker нам понадобится следующее:
В первую очередь нужно заказать облачный сервер.
1) Переходим на страницу авторизации и входим в аккаунт при помощи логина или адреса электронной почты и пароля или при помощи Passkey, ВКонтакте, GitHub, Google.
2) После успешной авторизации отобразится панель управления текущего проекта. Переходим в раздел «Облачные серверы» и нажимаем «Создать» или «Добавить».
3) Выбираем операционную систему, которая будет установлена на сервер. В нашем случае нам необходима Ubuntu версии 22.04.
Также в этом же разделе можно выбрать уже готовый образ с Docker перейдя во вкладку «Маркетплейс»:
4) Выбираем регион, в котором будет находиться наш сервер. Выбирать рекомендуется тот регион, который ближе всего находится к вам физически. У каждого доступного региона справа вверху отображается ping, т.е. время, необходимое для передачи данных с вашего компьютера на сервер. Чем меньше указанное время, тем быстрее будет осуществляться передача данных.
5) Далее выбираем необходимую конфигурацию для сервера. Так как в данной статье будут рассмотрены только тестовые примеры по работе с образами Docker, то для конфигурации сервера можно выбрать минимальную доступную конфигурацию, включающую в себя одноядерный процессор, 1 ГБ оперативной памяти и 15 ГБ места на NVMe диске. В реальности вам необходимо выбирать именно ту конфигурацию, которая будет удовлетворять вашим потребностям при работе с контейнерными приложениями и их образами.
Стоит отметить, что сам Docker потребляет минимум ресурсов. Расчет необходимой конфигурации производится исходя из той нагрузки и конфигурации, которую будет использовать контейнер. Выбираем соответствующий тариф:
6) Далее необходимо решить, будет ли сервер доступен из внешний сети или же только из приватной (частной) сети. Если не уверены в настройках, оставьте эти параметры без изменений.
7) По желанию можно оформить дополнительные услуги, включая резервные копии и защиту от DDoS-атак (последняя доступна только в Санкт-Петербурге и Москве).
8) Также заранее можно загрузить SSH-ключ, чтобы не входить на сервер при помощи пароля.
9) Можно задать необходимое имя для сервера которое будет отображаться в панели управления, а также выбрать проект.
10) Для создания сервера необходимо нажать на кнопку «Заказать»:
Если на вашем аккаунте недостаточно средств, то будет выведено предупреждение о необходимости пополнить баланс. После оплаты и создания сервера откроется Дашборд сервера, где можно будет найти IP-адрес, логин и пароль для подключения.
vds
Образы Docker создаются другими пользователями и хранятся в реестрах — специальных хранилищах для образов. Реестры бывают публичными и приватными. Публичные репозитории могут использовать все пользователи, при этом проходить аутентификацию не нужно. К частным реестрам могут получить доступ только те пользователи, у которых есть соответствующие данные для входа. Также частные репозитории широко используются компаниями для хранения собственных образов при разработке программного обеспечения.
По умолчанию Docker использует публичный реестр Docker Hub, который каждый пользователь может использовать как для публикации своих образов, так и для скачивания сторонних. Когда пользователь выполняет, например, команду docker run
, то по умолчанию демон Docker будет обращаться в свой стандартный реестр. При необходимости можно поменять реестр на другой — мы писали об этом в отдельной инструкции.
Для создания собственных Docker-образов используют Dockerfile — текстовый файл, внутри которого заданы инструкции по сборке образа. В качестве инструкций используются специально зарезервированные ключевые слова, которых насчитывается 18 штук. Среди наиболее распространенных типов инструкций можно выделить следующие:
FROM
— задает базовый образ. Любой образ начинается с базового. Под базовым образом понимают использование какого-либо дистрибутива Linux, включая Ubuntu, Debian, Oracle Linux, Alpine и т.д. Также существует большое количество образов с различными предустановленными программами, например, Nginx, Grafana, Prometheus, MySQL и др. Однако, даже если использовать образ с предустановленной программой, то внутри всегда будет задан какой-либо дистрибутив ОС Linux.WORKDIR
— создает директорию внутри образа. Функционал схож с утилитой mkdir
для создания каталог в дистрибутивах Linux. Можно использовать несколько раз в одном образе.COPY
— копирует файлы и каталоги внутрь образа с хостовой системы. Используется для копирования конфигурационных файлов и файлов исходного кода приложения.ADD
— функционал схож с инструкцией COPY
, однако помимо копирования файлов ADD
позволяет скачивать файлы с удаленных источников и разархивировать .tar
-архивы.RUN
— выполняет действия (команды) внутри образа. При помощи RUN
можно выполнять все те действия, которые пользователь может выполнять в оболочке Bash, включая создание файлов, установку пакетов, запуск сервисов и т.д.CMD
— задает команду, которая будет выполняться при запуске контейнера.Для примера создадим образ с простой программой на Python.
1) Создаем директорию проекта и переходим в нее:
mkdir python-calculator && cd python-calculator
2) Создаем файл console_calculator.py
со следующим содержимым:
print("*" * 10, "Calculator", "*" * 10)
print("To exit from program type q")
try:
while True:
arithmetic_operators = input("Choose arithmetic operation (+ - * /):\n")
if arithmetic_operators == "q":
break
if arithmetic_operators in ("+", "-", "*", "/"):
first_number = float(input("First number is:\n"))
second_number = float(input("Second number is:\n"))
print("The result is:")
if arithmetic_operators == "+":
print("%.2f" % (first_number + second_number))
elif arithmetic_operators == "-":
print("%.2f" % (first_number - second_number))
elif arithmetic_operators == "*":
print("%.2f" % (first_number * second_number))
elif arithmetic_operators == "/":
if second_number != 0:
print("%.2f" % (first_number / second_number))
else:
print("You can't divide by zero!")
else:
print("Invalid symbol!")
except (KeyboardInterrupt, EOFError) as e:
print(e)
3) Создаем новый файл с именем Dockerfile с содержимым:
FROM python:3.10-alpine
WORKDIR /app
COPY console_calculator.py .
CMD ["python3","console_calculator.py"]
Для базового образа выберем python:3.10
, который основан на легковесном дистрибутиве Linux под названием Alpine. Более подробно об использовании Alpine мы поговорим в следующей главе.
Внутри образа создадим директорию app
, в которой будет находиться файл проекта.
Запуск контейнера будет осуществляться при помощи команды "python3","console_calculator.py"
.
4) Для сборки образа используется команда docker build
. Также каждому образу должен быть присвоен тег. Тег — это уникальный идентификатор, который можно присвоить образу. Тег указывается через ключ -t
:
docker build -t python-console-calculator:01 .
Точка в конце команды означает, что Dockerfile находится в текущей директории.
Список созданных образов можно вывести, используя:
docker images
Запуск контейнера осуществляется при помощи команды:
docker run --rm -it python-console-calculator:01
Проверим функционал программы, выполнив несколько простых арифметических действий:
Для выхода из программы необходимо нажать на клавишу q
.
Так как при запуске контейнера мы указали ключ --rm
, то контейнер удалится автоматически.
Также контейнер можно запустить в режиме демона, т.е. в фоновом режиме. Для этого при запуске контейнера необходимо указать ключ -d
:
docker run -dit python-console-calculator:01
После этого контейнер отобразится в списке запущенных контейнеров:
При старте контейнера фоновом режиме с целью доступа к нашему скрипту необходимо воспользоваться docker exec
, которая выполняет команду внутри контейнера. Вначале необходимо запустить командную оболочку (bash
или shell
), далее в самом контейнере запустить скрипт вручную. Для этого выполняем команду docker exec
, передав в качестве аргумента команду sh
, которая откроет командную оболочку внутри контейнера (где 4f1b8b26c607
— это уникальный ID контейнера, который отображается в выводе столбца CONTAINER ID
команды docker ps
):
docker exec -it 4f1b8b26c607 sh
Далее запускаем скрипт вручную:
python console_calculator.py
Чтобы удалить запущенный контейнер, необходимо воспользоваться командой docker rm
, передав ей ID контейнера или имя. Также необходимо задать ключ -f
, при помощи которого работающий контейнер будет удален принудительно:
docker rm -f 186e8f43ca60
При создании Docker-образов придерживаются одного основного правила — готовые образы должны быть компактными и занимать как можно меньше места. Также чем меньше образ, тем быстрее он собирается. Это может сыграть ключевую роль при использовании в методиках CI/CD, а также при выпуске программного обеспечения по модели Time to market.
В качестве первой рекомендации необходимо грамотно выбирать базовый образ. Например, вместо использования различных образов дистрибутивов Linux, таких как Ubuntu, Oracle Linux, Rocky Linux и множество других, можно сразу выбрать тот образ, который уже поставляется вместе с необходимым языком программирования, фреймворком или другой необходимой технологией. К таким образам относят node для работы с платформой Node.js, готовый образ с Nginx, ibmjava для работы с языком программирования Java, postgres для работы с СУБД PostgreSQL, redis для работы с NoSQL Redis и многие другие.
Использование конкретного образа вместо образа операционной системы имеет следующие преимущества:
Нет необходимости устанавливать основной инструмент (язык программирования, фреймворк и т.д.), благодаря чему образ не будет «засорен» лишними пакетами, что не приведет к увеличению его размера.
Образы, которые поставляются с какой-либо предустановленной программой (Nginx, Redis, PostgreSQL, Grafana и т.д.), всегда создаются разработчиками самих программ. Это означает, что со стороны пользователя не нужно дополнительно настраивать саму программу, чтобы ее запустить (кроме случаев когда его необходимо интегрировать со своим сервисом).
Рассмотрим данную рекомендацию на практическом примере. В качестве примера воспользуемся простейшей программой на языке программирования Python, которая выводит фразу "Hello from Python!". Для начала соберем образ, используя debian
в качестве базового образа.
1) Создаем и переходим в директорию, в которой будут храниться файлы проекта:
mkdir dockerfile-python && cd dockerfile-python
2) Создаем файл test.py
со следующим содержимым:
print ("Hello from Python!")
3) Далее создаем Dockerfile со следующим содержимым:
FROM debian:latest
COPY test.py .
RUN apt update
RUN apt -y install python3
CMD ["python3", "test.py"]
Чтобы запускать программы на Python, также необходимо установить его интерпретатор.
4) Далее соберем образ:
docker build -t python-debian:01 .
Проверяем размер созданного образа:
docker images
Образ занимает 185MB, что довольно много для приложения, которое выводит в терминал одну строку.
Выберем правильный базовый образ, который основан на дистрибутиве Alpine.
Еще одной особенностью базовых образов является то, что для многих образов существуют специальные версии в виде slim- и alpine-образов, вес которых еще меньше. Рассмотрим на примере официального образа с Python версии 3.10. Образ python:3.10
занимает целый 1 ГБ, в то время как slim-версия намного меньше — 127 MB. А образ с alpine всего лишь 50 MB.
Slim-образы — это образы, в которых присутствует минимум пакетов, необходимых для запуска готового приложения. В таких образах отсутствует большая часть пакетов и библиотек. Slim создаются на основе как обычных дистрибутивов Linux, таких как Ubuntu, Debian, так и с помощью Alpine-дистрибутивов.
Alpine-образы — это образы, которые в качестве ОС используют дистрибутив Alpine — легковесный дистрибутив Linux, занимающий порядка 5 MB места на диске (без ядра). Отличается от других дистрибутивов Linux пакетным менеджером (используется apk
), отсутствием системы инициализации systemd
, а также меньшим числом предустановленных программ.
При использовании как Slim-, так и Alpine-образов необходимо тщательно протестировать ваше приложение, т.к. в таких дистрибутивах может отсутствовать необходимый пакет или библиотека.
Протестируем наше приложение, используя образ Python с Alpine.
1) Возвращаемся к ранее использованному Dockerfile и заменяем базовый образ с Debian на образ с python:alpine3.19
. Также необходимо убрать две инструкции RUN
, так как устанавливать интерпретатор Python не придется:
FROM python:alpine3.19
COPY test.py .
CMD ["python3", "test.py"]
2) Используем новый тег для сборки образа:
3) Проверяем размер нового образа и сравниваем с ранее созданным:
Так как мы выбрали правильный базовый образ с уже предустановленным Python, образ был уменьшен — с 185MB до 43.8МБ.
В основе Docker-образов лежит понятие слоя. Слой — это какое-либо внесенное изменение в файловую систему образа. К таким изменениям относят копирование/создание каталогов и файлов, а также установку пакетов. Рекомендуется использовать как можно меньше слоев в образе. Из всех существующих инструкций Dockerfile только инструкции FROM
, COPY
, ADD
, RUN
создают слои, увеличивающие итоговый размер образа. Все остальные инструкции создают временные промежуточные образы и не увеличивают размер образа напрямую.
В качестве примера возьмем ранее использованный Dockerfile, однако модифицируем его в соответствии с новыми потребностями. Предположим нам необходимо установить дополнительные пакеты при помощи менеджера пакетов apt
:
FROM debian:latest
COPY test.py .
RUN apt update
RUN apt -y install python3 htop net-tools mc gcc
CMD ["python3", "test.py"]
1) Собираем образ:
docker build -t python-non-optimize:01 .
2) Проверяем размер созданного образа:
docker images
Размер образа составил 570 МБ. Однако мы можем сократить размер путем использования меньшего количества слоев. Ранее в нашем Dockerfile было указано две инструкции RUN
, которые создали два слоя. Уменьшим размер образа путем объединения команд apt update
и apt install
, используя символы &&
, которые в Bash означают, что следующая команда будет выполнена только при условии завершения первой без ошибок. Также важным пунктом является удаление кэш-файлов, которые остаются в образе после установки пакетов при помощи пакетного менеджера apt
(данное утверждение применимо и к другим пакетным менеджерам — yum
/dnf
и apk
). Кэш должен быть удален. Для дистрибутивов, использующих apt
, кэш установленных программ хранится в директории /var/lib/apt/lists
, соответственно, добавим команду для удаления всех файлов в данной директории в инструкцию RUN
, не создавая новый слой:
FROM debian:latest
COPY test.py .
RUN apt update && apt -y install python3 htop net-tools mc gcc && rm -rf /var/lib/apt/lists/*
CMD ["python3", "test.py"]
3) Запускаем сборку измененного образа:
docker build -t python-optimize:03 .
4) И проверяем размер образа:
Размер образа был уменьшен с первоначального 570 MB до текущих 551 MB.
Еще одним существенным способом уменьшить размер создаваемого образа является использование multi-stage-сборок, они же многоступенчатые сборки, они же — использование двух или более базовых образов. Суть использования многоступенчатых образов заключается в том, что при сборке приложения остаются файлы, которые не нужны при запуске или функционировании используемого сервиса. К таким файлам можно отнести, например, сторонние библиотеки.
Рассмотрим на конкретном примере, используя платформу Node.js. Сам Node.js необходимо предварительно установить, воспользовавшись нашей инструкцией.
Соберем образ приложения без использования двух образов, чтобы оценить разницу в размере образа.
1) Создаем директорию для проекта:
mkdir node-app && cd node-app
2) Инициализируем новое приложение:
npm init -y
3) Устанавливаем библиотеку express
:
npm install express
3) Создаем файл index.js
со следующим содержимым:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(PORT, () => {
console.log(Server is running on port${PORT});
});
4) Создаем Dockerfile, используя содержимое:
FROM node:14-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY index.js .
EXPOSE 3000
CMD ["npm", "start"]
5) Для сборки образа используем команду:
docker build -t node-app:01 .
6) Смотрим размер образа:
docker images
7) Размер образа составил 124 MB. Перепишем Dockerfile для использования двух образов, приведя его к следующему виду:
FROM node:14 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY index.js .
FROM gcr.io/distroless/base-debian10 AS production
WORKDIR /app
COPY --from=builder /app .
EXPOSE 3000
CMD ["npm", "start"]
8) Запускаем сборку образа:
docker build -t node-app:02 .
9) После того как сборка будет завершена, сравним размер собранного образа:
docker images
По итогу размер образа колоссально был уменьшен — со 124 MB до 21.5 MB.
Выгодные тарифы на VDS/VPS<br>в Timeweb Cloud
В данной статье мы создали свой образ Docker и рассмотрели варианты его запуска. Также большое внимание было уделено оптимизации Docker-образов. При помощи оптимизации можно существенно уменьшить размер образа, что позволит производить сборку образов намного быстрее.