Сборка приложения Node.js с помощью Docker

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

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

Данный мануал поможет вам создать образ приложения для статического веб-сайта на фреймворке Express и Bootstrap. Мы с вами соберем контейнер с помощью полученного образа, а затем загрузим его на Docker Hub для дальнейшего использования. После этого мы загрузим образ с Docker Hub и соберем новый контейнер, чтобы понять, как работает воссоздание и масштабирование приложения.

Требования

1: Установка зависимостей приложения

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

Создайте каталог для вашего проекта в домашнем каталоге пользователя sudo. Мы назовем его node_project, но вы можете выбрать другое имя.

mkdir node_project

Перейдите в этот каталог:

cd node_project

Это будет корневой каталог проекта.

Затем создайте файл package.json с зависимостями вашего проекта и другой важной информацией. Откройте файл с помощью nano или другого редактора:

nano package.json

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

{
"name": "nodejs-image-demo",
"version": "1.0.0",
"description": "nodejs image demo",
"author": "Sammy the Shark <sammy@example.com>",
"license": "MIT",
"main": "app.js",
"keywords": [
"nodejs",
"bootstrap",
"express"
],
"dependencies": {
"express": "^4.16.4"
}
}

Этот файл включает в себя имя проекта, автора и лицензию, согласно которой он является общим. Npm рекомендует выбрать короткое и описательное имя для проекта и избегать дублирования в реестре npm. Мы указали лицензию MIT, которая разрешает свободное использование и распространение кода приложения.

Также файл определяет:

  • «main»: конечная точка приложения, app.js. Мы создадим этот файл немного позже.
  • «dependencies»: зависимости проекта. В этом случае это Express 4.16.4+.

Этот файл не указывает репозиторий, но вы можете добавить его, следуя рекомендациям по работе с package.json. Это хорошее дополнение, если вы собираетесь  отслеживать версии своего приложения.

Сохраните и закройте файл.

Чтобы установить зависимости проекта, выполните следующую команду:

npm install

Это установит в каталог проекта пакеты, которые вы перечислили в файле package.json.

2: Создание файлов приложения

Для примера мы создадим сайт, который предлагает пользователям информацию об акулах. Наше приложение будет иметь основную точку входа, app.js, и каталог views, который будет хранить статические активы проекта. Целевая страница index.html предложит пользователям некоторую базовую информацию и ссылку на страницу sharks.html с более подробной информацией. Мы создадим  обе страницы в каталоге views.

Для начала откройте app.js в каталоге проекта, чтобы определить маршруты проекта.

nano app.js

Первая часть файла создаст приложение Express и объекты Router и определит базовый каталог, порт и хост как переменные:

var express = require("express");
var app = express();
var router = express.Router();
var path = __dirname + '/views/';
const PORT = 8080;
const HOST = '0.0.0.0';

Функция require загружает модуль express, который затем используется для создания объектов app и router. Объект router будет выполнять функцию маршрутизации приложения; когда мы определим маршруты HTTP, мы добавим их в этот объект, чтобы определить, как приложение будет обрабатывать запросы.

Этот раздел файла также устанавливает несколько переменных: path, PORT и HOST.

  • path: определяет базовый каталог, в данном случае это подкаталог views в каталоге проекта.
  • HOST: определяет адрес, к которому приложение будет привязано и который оно будет прослушивать. Установка этого значения в 0.0.0.0 (все IPv4-адреса) соответствует поведению Docker по умолчанию – он присваивает контейнерам 0.0.0.0, если не указано иное.
  • PORT: определяет порт, к которому привязано приложение (8080).

С помощью объекта router определите маршруты приложения:

...
router.use(function (req,res,next) {
console.log("/" + req.method);
next();
});
router.get("/",function(req,res){
res.sendFile(path + "index.html");
});
router.get("/sharks",function(req,res){
res.sendFile(path + "sharks.html");
});

router.use загружает функцию промежуточного программного обеспечения, которая регистрирует запросы маршрутизатора и передает их маршрутам приложения. Они определены в последующих функциях, которые указывают, что запрос GET к базовому URL-адресу проекта должен возвращать страницу index.html, а запрос GET к маршруту /sharks должен возвращать страницу sharks.html.

Смонтируйте промежуточное программное обеспечение router и статические ресурсы приложения и настройте прослушивание порта 8080:

...
app.use(express.static(path));
app.use("/", router);
app.listen(8080, function () {
console.log('Example app listening on port 8080!')
})

В результате файл app.js выглядит так:

var express = require(«express»);
var app = express();
var router = express.Router();
var path = __dirname + ‘/views/’;
const PORT = 8080;
const HOST = ‘0.0.0.0’;
router.use(function (req,res,next) {
console.log(«/» + req.method);
next();
});
router.get(«/»,function(req,res){
res.sendFile(path + «index.html»);
});
router.get(«/sharks»,function(req,res){
res.sendFile(path + «sharks.html»);
});
app.use(express.static(path));
app.use(«/», router);
app.listen(8080, function () {
console.log(‘Example app listening on port 8080!’)
})

Сохраните и закройте файл.

Теперь давайте добавим контент приложения. Создайте каталог views :

mkdir views

Откройте файл посадочной страницы index.html:

nano views/index.html

Добавьте в файл следующий код, который импортирует Bootstrap и создаст компонент jumbotron со ссылкой на страницу с более подробной информацией sharks.html:

<!DOCTYPE html>
<html lang="en">
<head>
<title>About Sharks</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="css/styles.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Merriweather:400,700" rel="stylesheet" type="text/css">
</head>
<body>
<nav class="navbar navbar-dark bg-dark navbar-static-top navbar-expand-md">
<div class="container">
<button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span>
</button> <a class="navbar-brand" href="#">Everything Sharks</a>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="active nav-item"><a href="/" class="nav-link">Home</a>
</li>
<li class="nav-item"><a href="/sharks" class="nav-link">Sharks</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="jumbotron">
<div class="container">
<h1>Want to Learn About Sharks?</h1>
<p>Are you ready to learn about sharks?</p>
<br>
<p><a class="btn btn-primary btn-lg" href="/sharks" role="button">Get Shark Info</a>
</p>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-6">
<h3>Not all sharks are alike</h3>
<p>Though some are dangerous, sharks generally do not attack humans. Out of the 500 species known to researchers, only 30 have been known to attack humans.
</p>
</div>
<div class="col-lg-6">
<h3>Sharks are ancient</h3>
<p>There is evidence to suggest that sharks lived up to 400 million years ago.
</p>
</div>
</div>
</div>
</body>
</html>

navbar верхнего уровня здесь позволяет пользователям переключаться между страницами Home и Sharks. В подкомпоненте navbar-nav мы используем класс Bootstrap active, чтобы указать текущую страницу. Мы также указали маршруты к статическим страницам, которые соответствуют маршрутам, определенным в app.js:

...
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="active nav-item"><a href="/" class="nav-link">Home</a>
</li>
<li class="nav-item"><a href="/sharks" class="nav-link">Sharks</a>
</li>
</ul>
</div>
...

Также мы создали ссылку на страницу shark на кнопке jumbotron.

...
<div class="jumbotron">
<div class="container">
<h1>Want to Learn About Sharks?</h1>
<p>Are you ready to learn about sharks?</p>
<br>
<p><a class="btn btn-primary btn-lg" href="/sharks" role="button">Get Shark Info</a>
</p>
</div>
</div>
...

Также тут есть ссылка на пользовательскую таблицу стилей:

...
<link href="css/styles.css" rel="stylesheet">
...

Эту таблицу стилей мы создадим в конце этого раздела.

Сохраните и закройте файл.

Подготовив посадочную страницу приложения, вы можете создать страницу sharks.html, которая предоставит заинтересованным пользователям дополнительную информацию.

Откройте файл:

nano views/sharks.html

Добавьте в файл следующий код, который импортирует Bootstrap и пользовательскую таблицу стилей и предложит пользователям подробную информацию:

<!DOCTYPE html>
<html lang="en">
<head>
<title>About Sharks</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link href="css/styles.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Merriweather:400,700" rel="stylesheet" type="text/css">
</head>
<nav class="navbar navbar-dark bg-dark navbar-static-top navbar-expand-md">
<div class="container">
<button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span>
</button> <a class="navbar-brand" href="/">Everything Sharks</a>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav mr-auto">
<li class="nav-item"><a href="/" class="nav-link">Home</a>
</li>
<li class="active nav-item"><a href="/sharks" class="nav-link">Sharks</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="jumbotron text-center">
<h1>Shark Info</h1>
</div>
<div class="container">
<div class="row">
<div class="col-lg-6">
<p>
<div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans.
</div>
<img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark">
</p>
</div>
<div class="col-lg-6">
<p>
<div class="caption">Other sharks are known to be friendly and welcoming!</div>
<img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark">
</p>
</div>
</div>
</div>
</html>

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

Сохраните и закройте файл.

Теперь создайте пользовательскую таблицу стилей CSS, на которую ссылаются index.html и sharks.html. Сначала нужно создать папку css в каталоге views:

mkdir views/css

Откройте таблицу стилей:

nano views/css/styles.css

Добавьте такой код, чтобы задать цвет и шрифт:

.navbar {
margin-bottom: 0;
}
body {
background: #020A1B;
color: #ffffff;
font-family: 'Merriweather', sans-serif;
}
h1,
h2 {
font-weight: bold;
}
p {
font-size: 16px;
color: #ffffff;
}
.jumbotron {
background: #0048CD;
color: white;
text-align: center;
}
.jumbotron p {
color: white;
font-size: 26px;
}
.btn-primary {
color: #fff;
text-color: #000000;
border-color: white;
margin-bottom: 5px;
}
img,
video,
audio {
margin-top: 20px;
max-width: 80%;
}
div.caption: {
float: left;
clear: both;
}

Кроме настройки шрифта и цвета этот файл также ограничивает размер изображений, задав максимальную ширину (max-width) в 80%. Это помешает им занять на странице больше места, чем нам хотелось бы.

Сохраните и закройте файл.

Создав файлы приложения и установив зависимости проекта, вы можете запустить приложение.

Если вы выполнили мануал по настройке сервера (ссылка в Требованиях), у вас включен брандмауэр, а он поддерживает только трафик SSH. Для разрешения трафика на порт 8080 выполните:

sudo ufw allow 8080

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

cd ~/node_project

Запустите приложение:

node app.js

В браузере откройте:

http://your_server_ip:8080

Вы попадете на посадочную страницу приложения.

Нажмите кнопку Get Shark Info, чтобы открыть вторую страницу приложения.

Теперь у вас есть запущенное приложение. Чтобы остановить сервер, нажмите Ctrl+C. Теперь можно перейти к созданию Dockerfile, который позволит нам пересоздать и масштабировать это приложение.

3: Написание Dockerfile

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

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

В корневом каталоге проекта создайте Dockerfile:

nano Dockerfile

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

Давайте используем образ node:10-alpine, так как на момент написания статьи это рекомендуемая LTS-версия Node.js. Образы alpine предоставляются проектом Alpine Linux и помогают уменьшить размер итогового образа. Больше информации об образах Alpine и их совместимости с проектами можно найти в разделе Image Variants на странице образов Node в Docker Hub.

Добавьте оператор FROM, чтобы создать базовый образ приложения.

FROM node:10-alpine

Этот образ включает Node.js и npm. Каждый Dockerfile должен начинаться с оператора FROM.

По умолчанию образ Node содержит пользователя без прав root, которого вы можете использовать для запуска (чтобы не запускать контейнер приложения от имени root). Рекомендуется избегать запуска контейнеров с правами root и ограничивать возможности внутри контейнера только теми функциями, которые требуются для запуска его процессов. Поэтому мы используем домашний каталог пользователя node в качестве рабочего каталога приложения и установим его в качестве пользователя внутри контейнера. Рекомендации по работе с образом Node можно найти здесь.

Чтобы точно настроить привилегии для кода приложения в контейнере, давайте создадим каталоги app и node_modules в /home/node. Это предоставит необходимые привилегии, что важно при создании модулей локальных нод в контейнере (npm install). Также нужно передать права на каталоги пользователю node:

...
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

Дополнительную информацию об объединении операторов RUN можно найти здесь.

Затем поместите рабочий каталог приложения в /home/node/app:

...
WORKDIR /home/node/app

Если WORKDIR не задан, Docker создаст его по умолчанию, поэтому рекомендуется установить его явно.

Затем скопируйте файлы package.json и package-lock.json (для npm 5+):

...
COPY package*.json ./

Добавление оператора COPY перед запуском npm install или копирование кода приложения позволяет использовать механизм кэширования Docker. На каждом этапе сборки Docker будет проверять наличие кэшированного уровня. Если изменить package.json, этот уровень будет пересобран, но если изменений не будет, этот оператор позволит Docker использовать существующий уровень образа и пропустить переустановку модулей.

После копирования зависимостей проекта можно запустить npm install:

...
RUN npm install

Скопируйте код приложения в рабочий каталог на контейнере:

...
COPY . .

Чтобы убедиться, что файлы приложения принадлежат пользователю без прав root, скопируйте права из каталога приложения в каталог контейнера:

...
COPY --chown=node:node . .
Set the user to node:
...
USER node

Откройте порт 8080 в контейнере и запустите приложение.

...
EXPOSE 8080
CMD [ "node", "app.js" ]

EXPOSE не публикует порт, а действует как способ документирования того, какие порты в контейнере будут опубликованы в среде выполнения. CMD выполняет команду для запуска приложения – в этом случае node app.js. Обратите внимание, в каждом Dockerfile должен быть только один оператор CMD. Если вы включите несколько CMD, в силу вступит только последний оператор.

Dockerfile позволяет настроить много разных полезных функций. Полный список инструкций можно найти в документации Dockerfile.

В итоге Dockerfile выглядит так:

FROM node:10-alpine
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
COPY package*.json ./
RUN npm install
COPY . .
COPY --chown=node:node . .
USER node
EXPOSE 8080
CMD [ "node", "app.js" ]

Сохраните и закройте файл.

Перед созданием образа приложения добавьте файл .dockerignore. Он работает аналогично файлу .gitignore и указывает, какие файлы и каталоги в проекте не следует копировать в контейнер.

Откройте файл .dockerignore:

nano .dockerignore

В файл добавьте модули локального узла, логи npm, файл Dockerfile и файл .dockerignore:

node_modules
npm-debug.log
Dockerfile
.dockerignore

Если вы работаете с Git, вы также можете добавить каталог .git и файл .gitignore.

Сохраните и закройте файл.

Теперь все готово, можно собрать образ приложения с помощью команды docker build. Флаг -t в сборке Docker позволяет выбрать образу запоминающееся имя. Поскольку мы собираемся отправить образ в Docker Hub, давайте включим в тег имя пользователя Docker Hub. Мы пометим образ как nodejs-image-demo, а вы можете заменить его другим именем. Не забудьте также заменить your_dockerhub_username вашим именем Docker Hub.

docker build -t your_dockerhub_username/nodejs-image-demo .

Точка указывает, что контекстом сборки является текущий каталог.

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

docker images
REPOSITORY                                         TAG                 IMAGE ID            CREATED             SIZE

your_dockerhub_username/nodejs-image-demo          latest              1c723fb2ef12        8 seconds ago       73MB


node                                               10-alpine           f09e7c96b6de        3 weeks ago        70.7MB

Теперь можно создать контейнер с этим образом, используя docker run. Мы включим три флага:

  • -p: публикует порт в контейнере и преобразовывает его в порт на хосте. Мы будем использовать порт 80 на хосте, но вы можете изменить его по мере необходимости, если на этом порту запущен другой процесс. Читайте документацию Docker о портах.
  • -d: запускает контейнер в фоновом режиме.
  • —name: позволяет присвоить контейнеру запоминающееся имя.

Запустите следующую команду:

docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo

Как только контейнер запустится, вы можете проверить список работающих контейнеров:

docker ps
CONTAINER ID        IMAGE                                                   COMMAND             CREATED             STATUS              PORTS                  NAMES
e50ad27074a7        your_dockerhub_username/nodejs-image-demo               "node app.js"       8 seconds ago       Up 7 seconds        0.0.0.0:80->8080/tcp   nodejs-image-demo

Теперь, когда контейнер работает, вы можете зайти в свое приложение, перейдя в браузере по адресу http://your_server_ip. Вы снова увидите посадочную страницу своего приложения.

4: Репозиторий для работы с образами

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

Для начала нужно войти в учетную запись Docker Hub:

docker login -u your_dockerhub_username -p your_dockerhub_password

Такая аутентификация создаст файл ~/.docker/config.json с учетными данными Docker Hub в домашнем каталоге вашего пользователя.

Теперь можно загрузить образ приложения на Docker Hub, используя созданный ранее тег:

docker push your_dockerhub_username/nodejs-image-demo

Давайте проверим работу реестра образов. Для этого остановите текущий контейнер приложения и образ и попробуйте восстановить их с помощью образа в репозитории.

Сначала просмотрите список работающих контейнеров:

docker ps
CONTAINER ID        IMAGE                                       COMMAND             CREATED             STATUS              PORTS                  NAMES
e50ad27074a7        your_dockerhub_username/nodejs-image-demo   "node app.js"       3 minutes ago       Up 3 minutes        0.0.0.0:80->8080/tcp   nodejs-image-demo

Используя CONTAINER ID, указанный в выводе, остановите контейнер работающего приложения. Обязательно замените условный идентификатор в команде своим значением.

docker stop e50ad27074a7

Теперь просмотрите все свои контейнеры, добавив флаг -а:

docker images -a

Вы увидите следующий вывод с именем вашего образа, your_dockerhub_username / nodejs-image-demo, вместе с образом node и другими образами из вашей сборки:

REPOSITORY                                           TAG                 IMAGE ID            CREATED             SIZE
your_dockerhub_username/nodejs-image-demo            latest              1c723fb2ef12        7 minutes ago       73MB
<none>                                               <none>              2e3267d9ac02        4 minutes ago       72.9MB
<none>                                               <none>              8352b41730b9        4 minutes ago       73MB
<none>                                               <none>              5d58b92823cb        4 minutes ago       73MB
<none>                                               <none>              3f1e35d7062a        4 minutes ago       73MB
<none>                                               <none>              02176311e4d0        4 minutes ago       73MB
<none>                                               <none>              8e84b33edcda        4 minutes ago       70.7MB
<none>                                               <none>              6a5ed70f86f2        4 minutes ago       70.7MB
<none>                                               <none>              776b2637d3c1        4 minutes ago       70.7MB
node                                                 10-alpine           f09e7c96b6de        3 weeks ago         70.7MB

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

docker system prune -a

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

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

Читайте также: Удаление образов, контейнеров и томов Docker

Теперь загрузите образ приложения с Docker Hub:

docker pull your_dockerhub_username/nodejs-image-demo

Просмотрите образы:

docker images
REPOSITORY                                     TAG                 IMAGE ID            CREATED             SIZE
your_dockerhub_username/nodejs-image-demo      latest              1c723fb2ef12        11 minutes ago      73MB

Теперь можете повторно собрать контейнер с помощью этой команды:

docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo

Просмотрите запущенные контейнеры:

docker ps
CONTAINER ID        IMAGE                                                   COMMAND             CREATED             STATUS              PORTS                  NAMES
f6bc2f50dff6        your_dockerhub_username/nodejs-image-demo               "node app.js"       4 seconds ago       Up 3 seconds        0.0.0.0:80->8080/tcp   nodejs-image-demo

Чтобы убедиться, что все работает, снова откройте http://your_server_ip. Вы должны увидеть свое приложение.

Заключение

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

Больше информации о Docker Compose и Docker Machine можно найти в мануалах:

Рекомендации по работе с контейнерами можно найти тут:

Tags: , , , ,