В посібнику ми з нуля проведемо докеризацію чату на 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
Хоч тут не дуже багато команд, зверніть увагу на деякі моменти:
- Файл починається з офіційного образу Docker для LTS-релізу Node на момент написання матеріалу. Краще явно вказувати версію, а не
node:lts
чиnode:latest
, щоб той, хто проводитиме збірку образу на іншому хості, працював з тією ж версією. - Рядок з
USER
вказує Docker виконати всі кроки збірки, а потім процес в контейнері якnode
user, тобто вбудований user, котрий не має особливих прав. Якби цього рядка не було, процес запускався б відroot
, а це суперечить правилам безпеки, особливо принципу мінімальних привілеїв. Багато посібників з Docker опускають цей крок для спрощення, але ми зробимо деякі додаткові кроки, щоб уникнути запуску відroot
, адже це дуже важливо. - Рядок з
WORKDIR
встановлює робочу директорію/srv/chat
, куди ми розмістимо файли нашого застосунку для всіх наступних кроків збірки, а потім і для контейнерів, створених з образу. Тека/srv
повинна бути доступною на будь-якій системі, яка відповідає Стандартам ієрархії файлової системи, де зазначено, що тека призначена для «специфічних для сайту даних, які обслуговуються системою», що чудово підійде для застосунку на Node.
Перейдемо до створення файлу docker-compose.yml
:
version: '3.7'
services:
chat:
build: .
command: echo 'ready'
volumes:
- .:/srv/chat
Тут також треба дещо пояснити:
- Рядок
version
вказує Docker Compose версію формату файлу, який ми використовуємо. На момент написання матеріалу версія 3.7 є останньою, однак старіші версії також працюватимуть тут добре. По факту, версії 2.x можуть навіть краще підходити. Все залежить від використання. - У файлі ми оголошуємо єдиний сервіс
chat
, створений зDockerfile
у поточній директорії, вказаній як.
. Все, що сервіс робить зараз — це виводитьready
. - Рядок
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
Пояснимо крок за кроком:
- Команда
RUN
зmkdir
таchown
(єдині команди, котрі нам треба запустити як root) створює робочу директорію та переконується, що вона належить node user. - Дві команди об'єднані в єдиному кроці
RUN
. У порівнянні з розділенням команд на декілька кроків, такий підхід зменшує кількість шарів в кінцевому образі. У нашому прикладі це не відіграє особливої ролі, однак використовувати якомога менше шарів — хороша звичка. Так можна зберегти багато місця на диску та час завантаження. Наприклад, ви встановлюєте пакет, розархівовуєте його, проводите збірку, встановлюєте, а потім очищуєте в один крок, а не створюєте окремі файли для кожного кроку. - Команда
COPY
в./
копіює пакети npm уWORKDIR
, який ми задали вище./
вказує Docker теку призначення. Ми копіюємо лише файли пакета, а не всю теку застосунку, тому що Docker кешуватиме результат командиnpm install
нижче та повторно виконує його, лише якщо зміняться файли пакета. Якби ми копіювали усі сирцеві файли та внесли зміни хоча б в один з них, кеш не спрацював би, навіть якщо потрібні пакети не змінилися Так ми отримаємо зайвийnpm install
в наступних збірках. - Прапор
--chown=node:node
дляCOPY
потрібен, щоб переконатися, що файли належать непривілейованомуnode
user, а не root, як за замовчуванням. - Крок
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
.
Фінальний код доступний за посиланням.
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
працюватиме як завжди, а це може бути корисним при роботі з одним сервісом.
Ще немає коментарів