Что такое Docker? Создание контейнера для приложения Node.js


Контекст и мотивация

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

Это извечная проблема в компьютерном мире. Знаменитый мем «на моей машине работает» показывает, что если какой-то код выполняется локально на машине разработчика, это не гарантирует, что та же программа будет корректно работать на другой машине или на сервере в производственной среде.

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

Если разработчик пишет код Python на своей машине и размещает его на Github, делая его общедоступным в интернете, а кто-то другой клонирует этот проект на своей машине и пытается его запустить, будет ли он работать? Только если все зависимости установлены и Python работает на своей правильной версии. А если проект был разработан на компьютере с Windows, а другой человек пытается запустить его на машине с Linux? Здесь также потребуется некоторая адаптация.

В примере с двумя разработчиками это не кажется большой проблемой, но в больших проектах, где работают сотни людей и существует несколько сред разработки, постановки и производства, это может стать кошмаром. В этой статье мы хотим дать обзор одного из способов решения этой проблемы — Docker. Чтобы иметь возможность следовать приведенному ниже руководству, вам потребуются базовые знания Node.js, систем Linux и REST API.

Виртуальные машины

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

Это хорошее решение, но оно не позволяет обеим системам работать одновременно. Для этого появился другой тип решения — виртуализация. Ресурсы одной машины (память, хранилище, процессор и т.д.) могут быть разделены между виртуальными машинами, которые являются имитацией других компьютеров. Такое разделение ресурсов осуществляется специальным типом программного обеспечения, называемым гипервизором. И даже при виртуализации у нас все еще есть операционная система машины по умолчанию, которая называется хост-системой (host OS). И гипервизор установлен на ней.

Гипервизор способен выполнить следующее разделение: выделить 2 ГБ памяти, 100 ГБ дискового пространства и 2 ядра процессора для системы Linux (Ubuntu) и 4 ГБ памяти, 200 ГБ дискового пространства и 4 ядра процессора для системы Windows, и все это на одном и том же оборудовании. Очевидно, что данное оборудование должно иметь достаточно ресурсов для работы виртуальных машин. Виртуализированные системы, работающие поверх гипервизора, называются гостевыми операционными системами.

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

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

Docker

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

Для более практического определения Docker — это приложение, которое вы устанавливаете на свою машину, как и любое другое, и которое имеет как интерфейс командной строки (CLI), так и графический интерфейс на рабочем столе. Оно позволяет упаковывать приложения в изолированные среды, называемые контейнерами. Правильно настроенный контейнер содержит все необходимое для запуска приложения, включая ранее упомянутые двоичные файлы и библиотеки.

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

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

Деталь, отличающая контейнеры от виртуальных машин, заключается в том, что ими можно легко обмениваться посредством их образов, которые представляют собой файлы, содержащие всю информацию о данном контейнере, и Docker использует их в качестве отправной точки для создания нового контейнера. Любой человек может отправлять и получать образы контейнеров и запускать их на движке docker на своих локальных машинах или в облачных средах.

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

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

Создание контейнера для приложения

Теперь давайте создадим контейнер для приложения Node.js с помощью Express и посмотрим на практике, как все это работает. Чтобы не отвлекаться на Docker, приложение будет очень простым — одна конечная точка, которая возвращает сообщение. Убедитесь, что на машине установлен Node и менеджер пакетов npm. Чтобы создать приложение, создайте новый каталог с выбранным вами именем и внутри него выполните следующие команды.

$ npm init -y
$ npm install express
Войти в полноэкранный режим Выйти из полноэкранного режима

Первая команда создает проект Node.js в текущем каталоге, запуская файл package.json. Вторая устанавливает Express, фреймворк, который мы используем для создания конечной точки REST. Затем создайте в корне проекта файл index.js со следующим кодом:

const express = require('express');

const app = express();

const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
    res.send('I S2 Containers');
});

app.listen(PORT, () => {
    console.log(`Node app running on port ${PORT}`)
});
Вход в полноэкранный режим Выйти из полноэкранного режима

Вот наше приложение Node.js! Единственная конечная точка GET, которая возвращает клиенту сообщение «I S2 Containers». Чтобы запустить сервер и сделать конечную точку доступной, выполните команду node index.js из корня проекта. Теперь можно вызвать http://localhost:3000/ прямо из браузера или любого HTTP-клиента, чтобы увидеть происходящее волшебство.

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

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

Теперь давайте напишем Dockerfile и проверим, что означает каждая команда

FROM node:17

WORKDIR /app

ENV PORT 3000

COPY package.json /app/package.json

RUN npm install

COPY . /app

CMD ["node", "index.js"]
Войти в полноэкранный режим Выйти из полноэкранного режима

FROM node:17 — Эта команда сообщает Docker, какой базовый образ мы используем для нашего приложения. Здесь важно упомянуть Docker Hub, который является удаленным репозиторием Docker в интернете, откуда пользователи могут загружать готовые образы. В нашем примере мы используем образ под названием node, который представляет собой образ контейнера, в котором уже установлены все необходимые нам зависимости Node.js, а также мы передаем тег 17, который является версией используемого Node. С помощью этой команды Docker понимает, что он начнет создавать контейнер из образа, который уже существует. Далее все команды в файле будут выполняться из этого базового образа. Каждый Dockerfile должен начинаться с команды FROM.

WORKDIR /app — Определяет основной каталог приложения внутри контейнера. Именно здесь будут применяться последующие команды. Контейнер имеет свою собственную файловую систему, и каталог /app будет находиться в корне этой файловой системы.

ENV PORT 3000 — Устанавливает переменную окружения PORT в значение 3000.

COPY package.json /app/package.json — Копирует файл package.json в наш ранее определенный рабочий каталог.

RUN npm install — Запускает команду установки зависимостей Node. Стоит помнить, что эта команда выполняется внутри каталога /app, который содержит файл package.json.

COPY /app — Копирует все содержимое локального корневого каталога в каталог нашего приложения.

CMD ["node", "index.js"] — Определяет команду по умолчанию, которая будет выполняться при запуске контейнера. Когда мы скажем Docker запустить наш образ как контейнер, он посмотрит на эту команду и поймет, что при запуске контейнера он выполнит команду node index.js, которая является командой, запускающей созданный нами HTTP-сервер.

Итак, теперь, когда у нас готов Dockerfile, мы можем создать наше изображение.

$ docker build --tag i-love-containers .
Вход в полноэкранный режим Выйти из полноэкранного режима

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

После выполнения команды в терминале будут показаны логи того, что сделал Docker. Ясно, что он выполняет команды, указанные в Dockerfile. И теперь, когда мы собрали наш образ, просто используйте команду docker images в терминале, чтобы увидеть образы, доступные на машине. Когда образ готов, давайте запустим его как контейнер.

$ docker run -p 5000:3000 -d i-love-containers
Вход в полноэкранный режим Выйти из полноэкранного режима

Параметр -p 5000:3000 используется для указания того, что порт 3000 контейнера должен быть сопоставлен с портом 5000 машины, на которой запущен Docker. То есть, для доступа к нашей конечной точке на локальной машине мы используем http://localhost:5000/. Это свидетельствует о независимости контейнера от остального компьютера, ему необходимо явно знать порт, который мы собираемся запросить. Параметр -d — это запуск в режиме detach, что означает, что процесс будет запущен в фоновом режиме.

Теперь мы можем запустить docker ps, чтобы посмотреть, какие контейнеры запущены. Обратите внимание, что docker дал вашему контейнеру имя, что-то случайное, в колонке NAMES. Эта команда показывает только текущие запущенные контейнеры, а чтобы показать все доступные контейнеры, включая неактивные, используйте docker ps -a.

Вызвав конечную точку на порт 5000, мы видим, что она возвращает ожидаемое сообщение: наше приложение запущено внутри контейнера. Важно отметить, что Node, установленный локально на нашей машине, не запущен, только тот, который находится в контейнере.

Вы можете остановить работу контейнера командой docker stop <container name> и аналогичным образом запустить его снова командой docker start.

Развертывание

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

С помощью образа Docker можно развернуть один и тот же контейнер на нескольких облачных платформах, таких как Heroku, AWS, Google Cloud и других. Тема развертывания контейнеров довольно обширна и заслуживает отдельного поста, посвященного только этому. Пока же интересно знать, что все основные облачные платформы имеют механизмы развертывания контейнеров, что делает ваше приложение очень адаптируемым с одной платформы на другую.

Почему Docker?

Во-первых, контейнеры намного легче в плане памяти и обработки данных по сравнению с виртуальной машиной, на которой нужно запускать целую операционную систему, поскольку контейнеры используют одну и ту же ОС, используемую движком Docker. Если быть еще более точным, они используют одно и то же ядро, в отличие от виртуальных машин, у каждой из которых есть свое собственное.

Для тех, кто не знаком с этим термином, ядро — это мозг операционной системы, это часть программного обеспечения, которая взаимодействует с аппаратным обеспечением. Когда мы говорим о системе Linux, мы фактически говорим о системе, использующей ядро Linux, а существует несколько операционных систем, использующих это ядро. Система, использующая ядро Linux, обычно называется дистрибутивом Linux, например Ubuntu, CentOS, Kali и другие. При создании виртуальной машины необходимо создавать ядро с нуля, что гораздо более громоздко, чем просто запустить контейнер Docker, который уже использует ресурсы ядра оборудования.

Здесь стоит упомянуть о небольшом недостатке Docker. Поскольку контейнеры используют одно и то же ядро, можно запускать только те контейнеры, которые основаны на образах одной и той же ОС. Таким образом, мы можем запускать контейнеры на базе Linux только на Linux-машинах, и то же самое для Windows и MacOS. Контейнер с образом Windows не будет работать на Docker, установленном на Linux, и наоборот.
Как мы видели в примере, это не такая уж большая проблема, поскольку можно запускать Docker внутри WSL 2, работающей под Windows. Существует несколько механизмов, позволяющих обойти эту проблему. Одним из самых больших примеров использования Docker является развертывание приложений в облачных средах, где чаще всего используется Linux.

В настоящее время многие компании используют контейнеры для архитектур микросервисов, где части системы разделяются на более мелкие приложения с четко определенными обязанностями. Это облегчает обслуживание, тестирование и понимание сложных систем. Мы можем иметь контейнер, в котором работает Node.js, другой — PostgreSQL или другая база данных, третий — front-end приложение с React, все в рамках одной бизнес-логики, но разделенные на независимые контейнеры, каждый со своими стратегиями развертывания и деталями.

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

Чтобы отдать должное, эта статья была написана под впечатлением от видеоролика NetworkChuck на YouTube.

Оцените статью
devanswers.ru
Добавить комментарий