Досвід контейнеризації Node.js застосунків з Docker

27 хв. читання

В посібнику ми з нуля проведемо докеризацію чату на socket.io.

Оглянемо такі пункти:

  • Запуск Node.js застосунку в Docker;
  • Чому запускати все від імені root — погана практика;
  • Використання прив'язок для скорочення циклу тестування -> редагування -> перезавантаження;
  • Управління node_modules в контейнері (та які для цього є трюки);
  • Забезпечення повторюваних збірок з package-lock.json;
  • Dockerfile для середовищ розробки та продакшену з багаторівневою збіркою.

Для розуміння матеріалу вам потрібні певні навички роботи з Docker та Node.js. Щоб поступово ознайомитись з Docker, зверніться до офіційної документації.

Почнемо

У посібнику ми налаштовуватимемо все з нуля. Повний код для кожного етапу опублікований на GitHub, він більш деталізований. Якщо ви хочете слідувати за матеріалом, код першого етапу доступний тут.

Почнемо з встановлення Node та інших залежностей, потім запустимо npm init для створення нового пакета. Нічого не заважає зробити це зараз, однак ми засвоїмо більше, якщо використовуватимемо Docker з самого початку (не забуваймо, що основна причина використання Docker — те, що вам зовсім не потрібно встановлювати щось на хості).

Створимо «контейнер запуску», який матиме встановлений Node, а потім використаємо його для налаштування npm-пакетів застосунку.

Контейнер запуску та сервіс

Нам знадобляться два файли: Dockerfile та docker-compose.yml. Почнемо з Dockerfile:

FROM node:10.16.3

USER node

WORKDIR /srv/chat

Хоч тут не дуже багато команд, зверніть увагу на деякі моменти:

  1. Файл починається з офіційного образу Docker для LTS-релізу Node на момент написання матеріалу. Краще явно вказувати версію, а не node:lts чи node:latest, щоб той, хто проводитиме збірку образу на іншому хості, працював з тією ж версією.
  2. Рядок з USER вказує Docker виконати всі кроки збірки, а потім процес в контейнері як node user, тобто вбудований user, котрий не має особливих прав. Якби цього рядка не було, процес запускався б від root, а це суперечить правилам безпеки, особливо принципу мінімальних привілеїв. Багато посібників з Docker опускають цей крок для спрощення, але ми зробимо деякі додаткові кроки, щоб уникнути запуску від root, адже це дуже важливо.
  3. Рядок з WORKDIR встановлює робочу директорію /srv/chat, куди ми розмістимо файли нашого застосунку для всіх наступних кроків збірки, а потім і для контейнерів, створених з образу. Тека /srv повинна бути доступною на будь-якій системі, яка відповідає Стандартам ієрархії файлової системи, де зазначено, що тека призначена для «специфічних для сайту даних, які обслуговуються системою», що чудово підійде для застосунку на Node.

Перейдемо до створення файлу docker-compose.yml:

version: '3.7'

services:
  chat:
    build: .
    command: echo 'ready'
    volumes:
      - .:/srv/chat

Тут також треба дещо пояснити:

  1. Рядок version вказує Docker Compose версію формату файлу, який ми використовуємо. На момент написання матеріалу версія 3.7 є останньою, однак старіші версії також працюватимуть тут добре. По факту, версії 2.x можуть навіть краще підходити. Все залежить від використання.
  2. У файлі ми оголошуємо єдиний сервіс chat, створений з Dockerfile у поточній директорії, вказаній як .. Все, що сервіс робить зараз — це виводить ready.
  3. Рядок volume: .:/srv/chat вказує Docker прив'язати точку монтування поточної директорії на хості в /srv/chat в контейнері, тобто це WORKDIR, який ми встановили у Dockerfile вище. Тепер зміни сирцевих файлів на хості автоматично оновлюватимуть вміст контейнера, і навпаки. У розробці надзвичайно важливо зберігати цикл тестування -> редагування -> перезавантаження якомога коротшим. Однак тоді виникнуть проблеми з завантаженням npm-залежностей.

Тепер ми готові до збірки та тестування нашого контейнера. Коли ми запустимо docker-compose build, Docker створить образ з встановленим Node, як вказано у Dockerfile. Потім docker-compose up запустить контейнер з цим образом та виконає команду echo. Так ми дізнаємось, що все працює добре:

$ docker-compose build
Building chat
Step 1/3 : FROM node:10.16.3
# ... інший вивід з процесу збірки ...
Successfully built d22d841c07da
Successfully tagged docker-chat-demo_chat:latest

$ docker-compose up
Creating docker-chat-demo_chat_1 ... done
Attaching to docker-chat-demo_chat_1
chat_1  | ready
docker-chat-demo_chat_1 exited with code 0

Якщо ви отримали такий результат, все успішно налаштовано. 🎉

Ініціалізація npm-пакета

⚠️ Користувачам Linux: щоб наступний крок працював без проблем, node user в контейнері повинен мати той самий uid (ідентифікатор користувача), що і користувач хоста. Усе тому, що користувачу контейнера потрібні дозволи для зчитування та запису файлів на хості через bind mount та навпаки. В додатку до статті ви знайдете пораду щодо того, як розв'язати цю проблему. Користувачі Docker на Mac можуть не турбуватися про це, оскільки зіставлення uid відбувається за лаштунками. Але Docker на Linux більш продуктивний.

Тепер у нас є налаштоване середовище у Docker і ми готові встановити npm-пакети. Для цього запустимо інтерактивну оболонку в контейнері для сервісу chat та встановимо npm-пакети:

$ docker-compose run --rm chat bash
node@467aa1c96e71:/srv/chat$ npm init --yes
# ... створює package.json ...
node@467aa1c96e71:/srv/chat$ npm install
# ... створює package-lock.json ...
node@467aa1c96e71:/srv/chat$ exit

Створені файли розташовані на хості та готові до коміту в систему контролю версій:

$ tree
.
├── Dockerfile
├── docker-compose.yml
├── package-lock.json
└── package.json

Фінальний код доступний за посиланням.

Встановлення залежностей

Далі в нашому плані — встановлення залежностей застосунку. Ми хочемо встановити їх всередині контейнера через Dockerfile, щоб контейнер мав все для запуску застосунку. Це означає, що нам треба отримати файли package.json та package-lock.json в образі та запустити npm install в Dockerfile. Зміни будуть такими:

diff --git a/Dockerfile b/Dockerfile
index b18769e..d48e026 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,14 @@
 FROM node:10.16.3

+RUN mkdir /srv/chat && chown node:node /srv/chat
+
 USER node

 WORKDIR /srv/chat
+
+COPY --chown=node:node package.json package-lock.json ./
+
+RUN npm install --quiet
+
+# TODO: Можна прибрати, якщо у нас вже є залежності у package.json.
+RUN mkdir -p node_modules

Пояснимо крок за кроком:

  1. Команда RUN з mkdir та chown (єдині команди, котрі нам треба запустити як root) створює робочу директорію та переконується, що вона належить node user.
  2. Дві команди об'єднані в єдиному кроці RUN. У порівнянні з розділенням команд на декілька кроків, такий підхід зменшує кількість шарів в кінцевому образі. У нашому прикладі це не відіграє особливої ролі, однак використовувати якомога менше шарів — хороша звичка. Так можна зберегти багато місця на диску та час завантаження. Наприклад, ви встановлюєте пакет, розархівовуєте його, проводите збірку, встановлюєте, а потім очищуєте в один крок, а не створюєте окремі файли для кожного кроку.
  3. Команда COPY в ./ копіює пакети npm у WORKDIR, який ми задали вище. / вказує Docker теку призначення. Ми копіюємо лише файли пакета, а не всю теку застосунку, тому що Docker кешуватиме результат команди npm install нижче та повторно виконує його, лише якщо зміняться файли пакета. Якби ми копіювали усі сирцеві файли та внесли зміни хоча б в один з них, кеш не спрацював би, навіть якщо потрібні пакети не змінилися Так ми отримаємо зайвий npm install в наступних збірках.
  4. Прапор --chown=node:node для COPY потрібен, щоб переконатися, що файли належать непривілейованому node user, а не root, як за замовчуванням.
  5. Крок npm install виконуватиметься від node user в робочій директорії, щоб встановити залежності у /srv/chat/node_modules всередині контейнера.

Останній крок — саме те, що нам було потрібно, однак він спричиняє проблему, коли ми проводимо bind mount теки застосунку на хості через /srv/chat. На жаль, тека node_modules не існує на хості, тому прив'язка приховає node_modules, які ми встановили в образ. Кінцевий крок mkdir -p node_modules та наступна частина присвячені обробці таких випадків.

Трюк з томом node_modules

Існує декілька способів обійти проблему з прихованими node_modules. Для цього нам треба лише додати декілька рядків до файлу docker compose:

diff --git a/docker-compose.yml b/docker-compose.yml
index c9a2543..799e1f6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,3 +6,7 @@ services:
     command: echo 'ready'
     volumes:
       - .:/srv/chat
+      - chat_node_modules:/srv/chat/node_modules
+
+volumes:
+  chat_node_modules:

Команда chat_node_modules:/srv/chat/node_modules задає іменований том chat_node_modules, який містить директорію /srv/chat/node_modules у контейнері. У volumes: ми вказуємо усі іменовані томи, тому саме туди ми додаємо створений том chat_node_modules.

Видається, що все просто, однак нам треба зробити ще дещо, аби змусити код працювати.

  • Під час збірки, npm install встановлює залежності (які ми додамо в наступній частині) у /srv/chat/node_modules всередині образу.
/srv/chat$ tree # в образі
.
├── node_modules
│   ├── accepts
...
│   └── yeast
├── package-lock.json
└── package.json
  • Коли ми пізніше запускаємо контейнер з цього образу, використовуючи наш compose-файл, Docker спочатку прив'язує теку застосунку з хоста в контейнер як /srv/chat.
/srv/chat$ tree # в контейнері без тома node_modules
.
├── Dockerfile
├── docker-compose.yml
├── node_modules
├── package-lock.json
└── package.json

Проблема в тому, що node_modules в образі приховані, тому всередині контейнера ми бачимо лише пусту теку node_modules.

  • Далі Docker створює том, який містить копію /srv/chat/node_modules в образі, та монтує його в контейнер. Це у свою чергу, приховує node_modules від прив'язки на хості:
/srv/chat$ tree # в контейнері без тома node_modules
.
├── Dockerfile
├── docker-compose.yml
├── node_modules
│   ├── accepts
...
│   └── yeast
├── package-lock.json
└── package.json

Ось і бажаний результат: наші сирцеві файли об'єднані на хості в контейнер, тож ми можемо швидко внести зміни. До того ж залежності доступні всередині контейнера, тому ми можемо використовувати їх для запуску застосунку.

Тепер пояснимо останній крок mkdir -p node_modules у Dockerfile вище: насправді ми ще не встановили жодних пакетів, тому npm install не створює теку node_modules під час збірки. Коли Docker створює том /srv/chat/node_modules, він автоматично створює для нас теку, власником якої буде root, а це означає, що node user не зможе записувати в неї. Ми можемо запобігти цьому, створивши node_modules як node user під час збірки. Щойно ми встановили пакети, команда стає непотрібною.

Встановлення пакетів

Проведемо повторну збірку образу, щоб все було готово для встановлення пакетів:

$ docker-compose build
... збірка та запуск npm install (поки без пакетів)...

Для нашого чату потрібен express, тому ми виконаємо в оболонці npm install з прапором --save, щоб зберегти залежності в package.json та оновити відповідно package-lock.json:

$ docker-compose run --rm chat bash
Creating volume "docker-chat-demo_chat_node_modules" with default driver
node@241554e6b96c:/srv/chat$ npm install --save express
# ...
node@241554e6b96c:/srv/chat$ exit

Файл package-lock.json (який для більшості випадків замінював старіший npm-shrinkwrap.json) потрібен, аби впевнитись, що збірки образу Docker повторювані. Він записує версії усіх прямих та непрямих залежностей, аби переконатись, що результатом npm install в збірках Docker на різних хостах буде те саме дерево залежностей.

Варто відзначити, що встановлені node_modules не розташовані на хості. Там повинна бути пуста тека node_modules, створена в результаті стороннього ефекту зв'язування та створених томів. Справжні файли лежать у томі chat_node_modules. Якщо ми запустимо іншу оболонку в контейнері chat, ми знайдемо їх там:

$ ls node_modules
# на хості нічого
$ docker-compose run --rm chat bash
node@54d981e169de:/srv/chat$ ls -l node_modules/
total 196
drwxr-xr-x 2 node node 4096 Aug 25 20:07 accepts
# ... багато node modules в контейнері
drwxr-xr-x 2 node node 4096 Aug 25 20:07 vary

Для наступного запуску docker-compose build модулі будуть встановлені в образ.

Повний код можна знайти за посиланням..

Запуск застосунку

Ми нарешті готові встановити застосунок. Скопіюємо сирцеві файли, які залишились, а саме index.js та index.html.

Далі встановлюємо пакет socket.io. Зараз, під час написання матеріалу, цей застосунок сумісний лише з socket.io версії 1, тому вкажемо саме цю версію:

$ docker-compose run --rm chat npm install --save socket.io@1
# ...

В нашому файлі docker compose ми заміняємо тестову команду echo ready на команду запуску сервера застосунку. Нарешті, вказуємо Docker Compose запустити процес на порті 3000, щоб ми могли відкрити застосунок у браузері:

diff --git a/docker-compose.yml b/docker-compose.yml
index 799e1f6..ff92767 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,7 +3,9 @@ version: '3.7'
 services:
   chat:
     build: .
-    command: echo 'ready'
+    command: node index.js
+    ports:
+      - '3000:3000'
     volumes:
       - .:/srv/chat
       - chat_node_modules:/srv/chat/node_modules

Тепер ми готові виконати docker-compose up:

$ docker-compose up
Recreating dockerchatdemo_chat_1
Attaching to dockerchatdemo_chat_1
chat_1 | listening on *:3000

Застосунок запущено на http://localhost:3000.

Досвід контейнеризації Node.js застосунків з Docker

Фінальний код доступний за посиланням.

Docker для середовища розробки та продакшену

Тепер наш застосунок запущено в середовищі розробки завдяки docker compose. Перед тим, як ми зможемо використовувати його у продакшені, треба розв'язати декілька проблем та дещо виправити:

  • Зверніть увагу, що під час збірки контейнера там немає сирцевого коду застосунку — лише npm-пакети та залежності. Основна ідея контейнера у тому, що він повинен містити все необхідне для запуску застосунку, тож зрозуміло, що ми хочемо змінити це.
  • Тека /srv/chat застосунку зараз належить node user і записується від його імені. Більшість застосунків не перезаписують сирцеві файли під час виконання, тому, відповідно до принципу найменших привілеїв, ми не даємо їм на це право.
  • Образ досить великий (розміром 909MB за даними інструменту дослідження образів). Не варто занадто зациклюватись на розмірі образу, однак ми не хочемо витрачати ресурси дарма. Основна частина образу належить базовому образу node, який містить компілятор для збірки node modules, що використовують нативний код (а не чистий JavaScript). Ці інструменти не знадобляться нам під час виконання, тому — з міркувань безпеки і продуктивності — краще не залишати їх для продакшену.

На щастя, у Docker є потужний інструмент – багаторівнева збірка. Суть в тому, що ми вказуємо декілька команд FROM у Dockerfile, одну для кожного етапу, і на кожному з етапів можемо копіювати файли з попередніх етапів. Розглянемо, як це можна налаштувати:

diff --git a/Dockerfile b/Dockerfile
index d48e026..6c8965d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:10.16.3
+FROM node:10.16.3 AS development

 RUN mkdir /srv/chat && chown node:node /srv/chat

@@ -10,5 +10,14 @@ COPY --chown=node:node package.json package-lock.json ./

 RUN npm install --quiet

-# TODO: Можна видалити одразу, як з'являться деякі залежності у package.json.
-RUN mkdir -p node_modules
+FROM node:10.16.3-slim AS production
+
+USER node
+
+WORKDIR /srv/chat
+
+COPY --from=development --chown=root:root /srv/chat/node_modules ./node_modules
+
+COPY . .
+
+CMD ["node", "index.js"]
  • Наші кроки у Dockerfile сформують перший етап, який ми зараз назвали development, додавши AS development до рядка FROM на самому початку. Ми також позбулися тимчасового кроку mkdir -p node_modules, необхідного при запуску, оскільки тепер у нас встановлені пакети.
  • Новий етап починається з другого FROM, який витягує базовий образ node — slim для тої ж версії node та викликає етап production для ясності. Образ slimофіційний образ node від Docker. Він менший за образ node за замовчуванням, тому що не містить набір інструментів компілятора: там є лише системні залежності, потрібні для запуску node-застосунку, яких набагато менше.

Такий багаторівневий Dockerfile запускає npm install на першому етапі, де є повний образ вузла для збірки. Далі він копіює отриману теку node_modules для образу наступного етапу, який використовує базовий образ slim. Тож ми зменшили розмір продакшен-образу з 909MB до 152MB, а це економія приблизно в 6 разів при відносно невеликих зусиллях.

  • Знову ж, команда USER node вказує Docker запустити збірку та застосунок як непривілейований node user, а не root. Нам також треба повторно вказати WORKDIR, тому що він не зберігається автоматично на другому етапі.
  • Рядок COPY --from=development --chown=root:root ... копіює залежності, встановлені на попередньому етапі development, в етап продакшену та вказує root як власника, щоб node user мав права читання, але не запису.
  • Рядок COPY . . копіює інші файли застосунку з хосту до робочої директорії в контейнері, а саме /srv/chat.
  • Нарешті, крок CMD визначає команду для запуску. На етапі розробки, файли застосунку надходили з bind mount, налаштованого в docker-compose. Тож є сенс винести команду у файл docker-compose.yml, а не в Dockerfile. Для прикладу більш доречно буде залишити команду в Dockerfile, який збирає все в контейнер.

Нарешті, ми закінчили з налаштуваннями нашого багаторівневого. Dockerfile. Треба вказати, щоб Docker Compose використовував лише етап development, а не обробляв увесь Dockerfile. Тут нам допоможе параметр target:

diff --git a/docker-compose.yml b/docker-compose.yml
index ff92767..2ee0d9b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,9 @@ version: '3.7'

 services:
   chat:
-    build: .
+    build:
+      context: .
+      target: development
     command: node index.js
     ports:
       - '3000:3000'

Так ми зберегли у development старий процес, котрий був до того, як ми додали багатоетапну збірку.

Щоб зробити крок COPY . . у нашому Dockerfile безпечним, треба створити файл .dockerignore. Без нього COPY . . може захопити інші файли, які ми не хотіли б мати в продакшені нашого образу (наприклад, тека .git, node_modules, які встановлені на хості поза Docker, файли Docker, які належать збірці образу).

Проігнорувавши зазначені файли, розмір образів зменшиться, а процес збірки пришвидшиться — тому що демону Docker не потрібно буде навантажуватись, аби створити копії зайвих файлів для контексту збірки. Вміст .dockerignore матиме такий вигляд:

.dockerignore
.git
docker-compose*.yml
Dockerfile
node_modules

Після всіх налаштувань ми можемо запустити в продакшен збірку, щоб зімітувати, як CI-система може зібрати кінцевий образ, а потім запустити його, як це може зробити оркестратор:

$ docker build . -t chat:latest
# ... деталі збірки ...
$ docker run --rm --detach --publish 3000:3000 chat:latest
dd1cf2bf9496edee58e1f5122756796999942fa4437e289de4bd67b595e95745

І знову ж, перевіряйте свій застосунок на http://localhost:3000. Коли завершимо, ми можемо зупинити його, використовуючи ID контейнера.

$ docker stop dd1cf2bf9496edee58e1f5122756796999942fa4437e289de4bd67b595e95745

Налаштування nodemon на етапі розробки

Тепер, коли ми налаштували окремо development- та production-образи, розглянемо, як зробити образ development більш зручним для розробки, запустивши застосунок з nodemon для автоматичного перезавантаження контейнера при змінах в сирцевому файлі.

Спочатку виконаємо команду:

$ docker-compose run --rm chat npm install --save-dev nodemon

Так ми встановимо nodemon. Далі оновлюємо compose-файл, щоб запускати nodemon при запуску node:

diff --git a/docker-compose.yml b/docker-compose.yml
index 2ee0d9b..173a297 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,7 +5,7 @@ services:
     build:
       context: .
       target: development
-    command: node index.js
+    command: npx nodemon index.js
     ports:
       - '3000:3000'
     volumes:

Тепер ми використовуємо npx, щоб запустити nodemon через npm. Якщо запуск успішний, ми побачимо такий результат в консолі:

docker-compose up
Recreating docker-chat-demo_chat_1 ... done
Attaching to docker-chat-demo_chat_1
chat_1  | [nodemon] 1.19.2
chat_1  | [nodemon] to restart at any time, enter `rs`
chat_1  | [nodemon] watching dir(s): *.*
chat_1  | [nodemon] starting `node index.js`
chat_1  | listening on *:3000

Через Dockerfile ми додамо необхідні залежності у продакшен-образ. Якщо вихочете цього уникнути, доведеться створити ще один етап. Хоч Nodemon навряд чи потрібен в продакшені, однак деякі dev-залежності часто пропонують тестові утиліти, з якими ви можете запускати тести в продакшен-контейнері як частину CI.

$ docker-compose run --rm chat npm test

> chat@1.0.0 test /srv/chat
> echo "Error: no test specified" && exit 1

Error: no test specified
npm ERR! Test failed. See above for more details.

Повний код доступний за посиланням.

Висновок

Отже, ми з'ясували, як розгорнути застосунок в development- та production-середовищах всередині Docker.

Ми розглянули конфігурацію, яка дозволяє запустити середовище node, не встановлюючи нічого на хості. Ми також дізнались про трюки, що дозволяють уникнути запуску збірки та інших процесів від імені root, натомість запустили їх як непривілейований користувач з міркувань безпеки.

Особливість Node / npm зберігати всі залежності у теці node_modules трохи ускладнила наші налаштування, однак ми обійшли всі незручності, з вкладеним томом з node_modules.

Нарешті, ми запустили багатоетапну збірку в Docker, щоб створити Dockerfile, відповідний як для development-, так і production-середовищ.

Додаток: Проблема невідповідності UID на Linux

При використанні bind mount для поширення файлів між хостом на Linux та контейнером, ви, імовірно, матимете проблеми з дозволами, якщо числовий uid користувача в контейнері не відповідає користувачу на хості. Наприклад, контейнер може не мати прав читання або запису для файлів на хості, чи навпаки.

Якщо ж ваш uid на хості — 1000, проблем з докеризованим середовищем на node не буде. Усе тому, що офіційні образи Docker використовують uuid 1000 для node user. Ви можете перевірити ваш uid на хості через команду id.

UID зі значенням 1000 досить поширений, оскільки цей uid призначається при встановленні ubuntu. Якби ви змогли переконати всіх у своїй команді встановити свій uid як 1000, проблем не виникло б взагалі. Якщо так не вийде, то є ще декілька способів все налагодити:

  • Запустити сервіс як root у середовищі розробки, видаливши команду USER node з development в Dockerfile. Так ви впевнюєтесь, що користувач в контейнері (root) матиме права на читання та запис у файли на хості. Якщо користувач в контейнері створює якісь файли, вони будуть належати root на хості. Однак ви завжди можете виконати sudo chown -R your-user:your-group . на хості, щоб виправити проблему.

Ви досі можете (і повинні) запускати процес як непривілейований користувач у продакшені.

  • Використовуйте аргументи збірки в Dockerfile, щоб налаштувати UID та GID для node user під час процесу збірки. Для цього додамо декілька рядків до development в Dockerfile:
FROM node:10.16.3 AS development

ARG UID=1000
ARG GID=1000
RUN \\
  usermod --uid ${UID} node && groupmod --gid ${GID} node &&\\
  mkdir /srv/chat && chown node:node /srv/chat

# ...

Тут ми оголосили два аргументи збірки, UID та GID, які за замовчуванням приймають значення 1000, якщо не задано інших аргументів. Тож node user та група використовують ці ID перед створенням будь-яких файлів.

Кожен розробник, значення uid/gid якого відрізняється, повинен встановити їх як аргументи для Docker Compose. Це можна зробити за допомогою docker-compose.override.yml, який повинен ігноруватись системою контролю версій (тобто згадуватись в .gitignore).

version: '3.7'

services:
  chat:
    build:
      args:
        UID: '500'
        GID: '500'

Як бачимо, значення uid та gid в контейнері будуть встановлені як 500. Нагадаємо, що всі зміни повинні бути на етапі development.

Очікується, що будуть і простіші способи розв'язання цієї проблеми.

Примітки

  • В принципі, немає різниці, куди розміщуються файли в контейнері. Директорія /opt також добре підійде. Інший варіант — зберігати їх в /home/node, це спрощує отримання доступу до деяких файлів у середовищі розробки, однак потребує більше дій та має менше сенсу в продакшені. Краще призначати власником файлів застосунку root, щоб вони були доступні лише для читання. У будь-якому випадку тека /srv підійде.
  • Як 2.x, так і 3.x версії файлу Docker Compose досі активно розвиваються. Основна перевага версій 3.x — це те, що вони підтримують сумісність застосунків, запущених на одній ноді з Docker Compose та на декількох нодах з Docker Swarm. Для того, щоб зберегти сумісність, версії 3.x змушені позбутись деяких корисних фіч версій 2.x. Якщо вам потрібен лише Docker Compose, можливо, краще підійдуть останні формати 2.х версій.
  • Про деякі з трюків, які ми змушені були використати з Dockerfile, можна забути, якщо при збірці дозволити виконання кроку npm install як root. В такому випадку ми можемо використовувати непривілейованого користувача node під час виконання — з міркувань безпеки. Dockerfile для запуску збірки як root та контейнер для node user буде приблизно таким:
FROM node:10.16.3

WORKDIR /srv/chat

COPY package.json package-lock.json ./

RUN npm install --quiet

USER node

Такий код чистіший, команди mkdir та chown не потрібні, однак ціною буде запуск npm install як root на етапі збірки. Вам вирішувати: трохи ускладнити налаштування і не запускати збірку як root — чи зберегти простоту Dockerfile з усіма наслідками.

Варто пам'ятати, що при збірці від root, якщо ви захочете пізніше встановити нові залежності, вам потрібно запустити оболонку як root, а не node user: docker-compose run --rm --user root chat bash, а потім npm install --save express.

  • Як альтернативний варіант ми можемо використовувати анонімний том з модулями:
diff --git a/docker-compose.yml b/docker-compose.yml
index c9a2543..5a56364 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,3 +6,4 @@ services:
     command: echo 'ready'
     volumes:
       - .:/srv/chat
+      - /srv/chat/node_modules

Хоч такий варіант коротший, ви забудете очистити анонімні томи. До того ж не буде жодної вказівки, з якого вони контейнера. Ви досі можете очистити їх командою docker system prune, але це не зовсім слушний інструмент для такої задачі. Підхід з іменованими томами потребує більш зусиль, натомість він більш прозорий.

Вас може цікавити, де насправді зберігаються файли залежностей у модулі. Як з іменованими, так і з анонімними томами, згадані файли розміщуються в окремій директорії, що управляється Docker на хості. Детальніше в документації Docker.

  • Уважний читач міг помітити, що нам не треба виконувати docker-compose build, щоб встановити залежності перед docker-compose up. Усе тому, що ця команда відпрацьовує в модулі chat_node_modules. Коли наступного разу ми робимо збірку, npm з нуля встановлює залежності в образ, але щоб встановити пакети повсякденно, ми можемо просто виконати npm install в контейнері без потреби повторної збірки.

Якщо вам колись треба буде позбутися іменованого тому та почати з нуля, ви можете запустити docker volume list, щоб отримати перелік всіх томів. Повна назва вашого тому з node modules залежатиме від вашого проєкту docker compose проєкту. В прикладі нас цікавить docker-chat-demo_chat_node_modules, який може бути видаленим, якщо ми спочатку видалимо контейнер командою docker-compose rm -v chat, а потім сам том docker volume rm docker-chat-demo_chat_node_modules.

  • Docker також пропонує офіційний образ alpine, він менший за розміром. Така особливість спричинена використанням зовсім іншого libc та пакетного менеджера у порівнянні з образами на Debian. Усі пов'язані з цим проблеми варті того, лише якщо ви розгортаєте образ у вбудованій системі, де економія місця дуже важлива. До того ж образи на Debian вже пропонують суттєве зменшення розміру.

  • Ви могли помітити, що на зупинку контейнера пішло десь 10 секунд. Це тому, що приклад чату на socket.io не обробляє коректно сигнал SIGTERM, який Docker відправляє, щоб провести плавне завершення. Додайте такий код в кінець index.js:

process.on('SIGTERM', function() {
  io.close();
  Object.values(io.of('/').connected).forEach(s => s.disconnect(true));
});

Далі повторно проводимо збірку production-образу, пробуємо запустити та виконати docker stop для контейнера знову. Після змін він повинен одразу зупинитись.

  • Використання npm для запуску процесів в контейнерах іноді не рекомендується, однак це стосується старих версій npm, які мали проблеми з обробкою сигналів, потрібних для чистого завершення процесів. В останніх версіях все виправлено. Якщо вашому контейнеру завжди потрібно приблизно 10 секунд для завершення, наймовірніше, ви не обробляєте сигнал SIGTERM. Запуск процесів через npm спричиняє деякі додаткові витрати, а саме додатковий процес node. Це може бути зайвим в продакшені, однак в розробці стає у пригоді.
  • Ви могли помітити, що nodemon пропонує ввести rs для перезапуску. Ця команда не працюватиме, якщо ми використовуємо docker-compose up для запуску сервісу, оскільки наш термінал не з'єднаний зі стандартним вводом nodemon. Якщо ж ми запустимо docker-compose run --rm chat, то rs працюватиме як завжди, а це може бути корисним при роботі з одним сервісом.
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація