Пошук уроків, статей та іншого контенту
Контейнеризуєте Node.js застосунок із правильною структурою Dockerfile, скриптами запуску та обробкою залежностей.
Docker пакує застосунок разом із середовищем, у якому він запускається:
версією Node.js;
залежностями з package.json;
кодом застосунку;
командою запуску;
налаштуваннями середовища.
Завдяки цьому застосунок працює однаково локально, у CI/CD та на сервері.
Для Node.js важливо правильно організувати порядок побудови образу:
скопіювати файли залежностей;
встановити залежності;
скопіювати код застосунку;
визначити команду запуску.
Такий порядок дає змогу Docker повторно використовувати кешовані шари, якщо змінюється лише код, але не залежності.
Розглянемо простий HTTP-сервер на Express:
my-app/
├── src/
│ └── server.js
├── .dockerignore
├── Dockerfile
├── package.json
└── package-lock.jsonpackage.json{
"name": "docker-node-app",
"version": "1.0.0",
"private": true,
"description": "Приклад контейнеризованого Node.js застосунку",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js"
},
"dependencies": {
"express": "^4.21.2"
}
}Скрипт start призначений для запуску застосунку в контейнері. Скрипт dev можна використовувати локально під час розробки, але для production-запуску він не потрібен.
Встановіть залежності та створіть package-lock.json:
npm installФайл package-lock.json потрібно зберігати в репозиторії. Він фіксує конкретні версії залежностей і дає змогу відтворювано встановлювати їх у Docker-образі.
src/server.jsconst express = require('express');
const app = express();
const port = Number(process.env.PORT) || 3000;
app.get('/', (req, res) => {
res.json({
message: 'Застосунок працює в Docker-контейнері'
});
});
app.listen(port, '0.0.0.0', () => {
console.log(`Сервер запущено на порту ${port}`);
});Сервер слухає адресу 0.0.0.0, а не лише localhost. Це необхідно, щоб до нього можна було звернутися ззовні контейнера.
Створіть у корені проєкту файл Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./src
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
USER node
CMD ["npm", "start"]Розглянемо інструкції по черзі.
FROMFROM node:22-alpineFROM визначає базовий образ. У цьому прикладі використовується образ Node.js на основі Alpine Linux.
Тег версії краще вказувати явно. Це робить середовище передбачуванішим, ніж використання тегу latest.
WORKDIRWORKDIR /appВстановлює робочу директорію всередині контейнера. Наступні інструкції, зокрема COPY, RUN і CMD, виконуються відносно /app.
COPY package*.json ./Ця інструкція копіює package.json і package-lock.json у контейнер.
Файли залежностей копіюються окремо від коду, щоб Docker міг кешувати результат встановлення пакетів. Якщо зміниться src/server.js, але файли залежностей залишаться незмінними, шар із npm ci можна буде використати повторно.
RUN npm ci --omit=devnpm ci призначена для чистого встановлення залежностей у CI та контейнерах:
використовує package-lock.json;
видаляє наявну директорію node_modules;
встановлює саме зафіксовані версії;
працює передбачуваніше для автоматизованих збірок, ніж npm install.
Параметр --omit=dev не встановлює залежності з devDependencies, оскільки вони зазвичай не потрібні для запуску production-застосунку.
Якщо проєкт потребує інструментів із devDependencies під час збірки, наприклад TypeScript-компілятора, для нього краще використати окремий build-етап. У наведеному прикладі застосунок запускається без попередньої компіляції.
COPY src ./srcПісля встановлення залежностей копіюється вихідний код застосунку.
Не варто без потреби використовувати:
COPY . .Така команда може скопіювати в образ локальні залежності, файли журналів, секрети та інші непотрібні файли. Якщо потрібно копіювати весь проєкт, зайві файли слід виключити через .dockerignore.
ENV NODE_ENV=production
ENV PORT=3000NODE_ENV=production повідомляє застосунку, що він працює в production-режимі.
Значення змінних, які можуть відрізнятися між середовищами, краще передавати під час запуску контейнера. Наприклад, порт можна перевизначити без зміни Dockerfile.
EXPOSEEXPOSE 3000Документує порт, який використовує застосунок усередині контейнера. EXPOSE не публікує порт автоматично. Для доступу з хост-машини потрібно використати параметр -p під час запуску.
USER nodeОфіційний образ Node.js містить користувача node. Запуск застосунку не від імені root зменшує потенційні наслідки вразливостей у застосунку або його залежностях.
CMD ["npm", "start"]Це команда за замовчуванням під час запуску контейнера.
Масивний, або exec-синтаксис, є кращим для процесів у контейнері:
CMD ["npm", "start"]Порівняно із shell-синтаксисом:
CMD npm startexec-синтаксис коректніше передає сигнали головному процесу. Це важливо під час зупинки контейнера та коректного завершення роботи Node.js застосунку.
.dockerignoreСтворіть файл .dockerignore:
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore
.env
coverage
dist.dockerignore визначає файли, які не потрібно передавати в контекст збірки.
Особливо важливо виключити:
node_modules — залежності мають встановлюватися всередині контейнера;
.env — файл може містити секрети;
.git — історія Git не потрібна для запуску;
журнали та результати тестів;
локальні артефакти збірки.
Файл .dockerignore не замінює захист секретів. Секрети взагалі не слід додавати до контексту збірки або копіювати в образ.
Виконайте команду в корені проєкту:
docker build -t docker-node-app .Тут:
docker build запускає побудову образу;
-t docker-node-app задає ім’я образу;
. означає поточну директорію як контекст збірки.
Docker виконає інструкції з Dockerfile і створить образ із назвою docker-node-app.
Перевірити список локальних образів можна командою:
docker imagesЗапустіть контейнер:
docker run --rm -p 3000:3000 --name docker-node-app docker-node-appПараметри команди:
--rm автоматично видаляє контейнер після його зупинки;
-p 3000:3000 зіставляє порт хост-машини з портом контейнера;
--name docker-node-app задає ім’я контейнера;
останній аргумент — ім’я образу.
Після запуску застосунок буде доступний на:
http://localhost:3000У відповідь сервер поверне JSON:
{
"message": "Застосунок працює в Docker-контейнері"
}Зупинити контейнер можна комбінацією Ctrl+C.
Значення змінних середовища можна змінити без перебудови образу:
docker run --rm \
-p 8080:4000 \
-e PORT=4000 \
--name docker-node-app \
docker-node-appУ цьому випадку:
Node.js застосунок слухає порт 4000 усередині контейнера;
порт 4000 контейнера доступний через порт 8080 хост-машини.
Застосунок буде доступний на:
http://localhost:8080Синтаксис зіставлення портів має вигляд:
порт_хоста:порт_контейнераЗначення PORT потрібно передати узгоджено з портом контейнера, який використовує застосунок.
Переглянути запущені контейнери:
docker psПереглянути журнали:
docker logs docker-node-appПереглядати журнали в реальному часі:
docker logs -f docker-node-appЯкщо контейнер запущено у фоновому режимі, використовуйте -d:
docker run -d \
-p 3000:3000 \
--name docker-node-app \
docker-node-appЗупинити його можна так:
docker stop docker-node-appПісля зупинки контейнер не видаляється автоматично, якщо під час запуску не було вказано --rm. Видалити його можна командою:
docker rm docker-node-appПісля зміни коду образ потрібно перебудувати:
docker build -t docker-node-app .Потім запустіть новий контейнер:
docker run --rm -p 3000:3000 --name docker-node-app docker-node-appDocker використовуватиме кеш для незмінених шарів. Наприклад, якщо змінився лише src/server.js, повторно виконувати npm ci зазвичай не потрібно.
Якщо змінився package.json або package-lock.json, шар залежностей буде перебудовано:
COPY package*.json ./
RUN npm ci --omit=devЦе гарантує, що образ міститиме актуальні залежності.
npm install замість npm ciДля production-образів і автоматизованих збірок краще використовувати:
RUN npm ci --omit=devДля цієї команди потрібен актуальний package-lock.json. Якщо lock-файлу немає, спочатку виконайте npm install локально та додайте створений файл до репозиторію.
node_modulesНе копіюйте локальні залежності в образ:
COPY . .без відповідного .dockerignore.
Локальні node_modules можуть бути встановлені для іншої операційної системи або архітектури. Залежності мають встановлюватися всередині контейнера.
localhostТакий код не дасть доступу до сервера ззовні контейнера:
app.listen(port, '127.0.0.1');Потрібно слухати всі мережеві інтерфейси контейнера:
app.listen(port, '0.0.0.0');EXPOSE відкриває портІнструкція:
EXPOSE 3000лише описує порт. Вона не створює доступ із хост-машини.
Порт потрібно опублікувати під час запуску:
docker run -p 3000:3000 docker-node-approotЯкщо в Dockerfile не вказати користувача, процес часто запускається від імені root.
Для простого Node.js застосунку можна використовувати вбудованого користувача образу:
USER nodeНе додавайте .env, ключі та паролі до образу через COPY. Навіть якщо файл буде видалено в наступному шарі, його вміст може залишитися в історії шарів.
Конфігураційні значення передавайте під час запуску через -e або інші механізми керування середовищем.
Щоб контейнеризувати Node.js застосунок:
визначте production-скрипт start у package.json;
збережіть package-lock.json;
використовуйте npm ci для відтворюваного встановлення залежностей;
копіюйте файли залежностей до копіювання вихідного коду;
виключіть node_modules, .env та інші зайві файли через .dockerignore;
запускайте сервер на 0.0.0.0;
передавайте порт через docker run -p;
запускайте контейнер не від імені root;
використовуйте exec-синтаксис CMD ["npm", "start"];
перебудовуйте образ після змін у коді або залежностях.