Пошук уроків, статей та іншого контенту
Створите багатоступеневий Dockerfile, щоб відокремити складання від запуску та отримати компактний production-образ.
Під час складання застосунку часто потрібні:
компілятор або транспілятор;
пакети для розробки;
тести;
інструменти командного рядка;
вихідний код.
У production ці компоненти зазвичай не потрібні. Запущеному застосунку достатньо скомпільованих файлів і production-залежностей.
Багатоступеневий Dockerfile розділяє цей процес на кілька етапів:
встановлення залежностей;
складання застосунку;
підготовка runtime-оточення;
запуск готового результату.
Кожен етап починається з власного FROM. Фінальний образ містить лише те, що явно скопійовано до фінального етапу.
Розглянемо простий Node.js-застосунок із TypeScript:
project/
├── Dockerfile
├── .dockerignore
├── package.json
├── package-lock.json
├── tsconfig.json
└── src/
└── server.tsФайл package.json:
{
"name": "multi-stage-example",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "tsc",
"start": "node dist/server.js"
},
"dependencies": {
"express": "^5.1.0"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/node": "^24.0.0",
"typescript": "^5.8.3"
}
}Файл tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}Файл src/server.ts:
import express from "express";
const app = express();
const port = Number(process.env.PORT ?? 3000);
app.get("/", (_request, response) => {
response.json({
message: "Application is running",
environment: process.env.NODE_ENV ?? "development"
});
});
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});Перед складанням потрібно створити package-lock.json:
npm install# Етап із залежностями для складання
FROM node:22-bookworm-slim AS dependencies
WORKDIR /app
# Спочатку копіюємо лише файли залежностей для кращого кешування
COPY package.json package-lock.json ./
RUN npm ci
# Етап складання застосунку
FROM dependencies AS build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Фінальний production-етап
FROM node:22-bookworm-slim AS production
ENV NODE_ENV=production
ENV PORT=3000
WORKDIR /app
# Встановлюємо лише production-залежності
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
# Копіюємо тільки результат складання
COPY --from=build --chown=node:node /app/dist ./dist
# Запускаємо процес без root-привілеїв
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]Назви dependencies, build і production — це імена етапів. Їх задає конструкція:
FROM image AS stage-namedependenciesFROM node:22-bookworm-slim AS dependenciesЦей етап містить повне дерево залежностей:
production-залежності;
dev-залежності;
TypeScript;
типи для TypeScript.
Команда npm ci використовує package-lock.json і встановлює точно зафіксовані версії пакетів.
Файли залежностей копіюються окремо:
COPY package.json package-lock.json ./
RUN npm ciЦе важливо для кешування. Якщо зміниться файл у src, Docker зможе використати закешований шар із залежностями. Залежності буде встановлено повторно лише після зміни package.json або package-lock.json.
buildFROM dependencies AS buildЕтап build успадковує файлову систему етапу dependencies, зокрема каталог node_modules.
Далі копіюються конфігурація TypeScript і вихідний код:
COPY tsconfig.json ./
COPY src ./src
RUN npm run buildКомпілятор створює каталог dist:
dist/
└── server.jsУ фінальний образ не потрібно переносити весь вихідний код або інструменти TypeScript. Потрібен лише каталог dist.
productionFROM node:22-bookworm-slim AS productionФінальний етап починається з чистого базового образу. Він не успадковує автоматично файли з build.
Production-залежності встановлюються окремо:
RUN npm ci --omit=dev && npm cache clean --forceПараметр --omit=dev не встановлює пакети з devDependencies.
Результат складання переноситься командою:
COPY --from=build /app/dist ./dist--from=build означає, що джерелом копіювання є етап із назвою build, а не контекст складання на хості.
У фінальному образі будуть:
Node.js;
production-залежність express;
скомпільований файл dist/server.js;
мінімально необхідні метадані застосунку.
У ньому не буде:
TypeScript;
типів;
вихідних .ts-файлів;
повного дерева dev-залежностей.
.dockerignoreКонтекст складання також впливає на швидкість і розмір операцій COPY. Створіть файл .dockerignore:
node_modules
dist
.git
.gitignore
Dockerfile
npm-debug.log
.envКаталог node_modules не потрібно передавати з локальної машини: він створюється всередині етапу dependencies.
Файл .env не слід передавати в контекст, оскільки він може містити секрети.
Виконайте складання з кореня проєкту:
docker build -t multi-stage-example:1.0 .Запустіть контейнер:
docker run --rm -p 3000:3000 multi-stage-example:1.0Застосунок буде доступний на порту 3000.
Перевірити його можна командою:
curl http://localhost:3000Очікувана відповідь:
{"message":"Application is running","environment":"production"}Порівняти розмір готового образу можна командою:
docker images multi-stage-exampleЗа замовчуванням docker build створює останній етап Dockerfile — у прикладі production.
Певний етап можна створити за допомогою --target:
docker build --target build -t multi-stage-example:build .Це корисно, коли потрібно:
перевірити результат складання;
виконати окремі перевірки;
діагностувати помилку компіляції;
створити тимчасовий образ для розробки.
Образ, створений із --target build, міститиме dev-залежності та вихідний код. Це не production-образ.
У багатоступеневих Dockerfile є два основні варіанти копіювання.
COPY --from=build /app/dist ./distЦе найчастіший варіант. Він переносить лише вказаний каталог або файл.
--from також може посилатися на інший образ:
COPY --from=node:22-bookworm-slim /usr/local/bin/node /usr/local/bin/nodeОднак для звичайного застосунку краще явно вказувати базовий образ у FROM, щоб Dockerfile залишався зрозумілим і передбачуваним.
Docker кешує результати інструкцій, якщо їхні вхідні дані не змінилися.
Невдалий порядок:
COPY . .
RUN npm ciУ цьому випадку зміна будь-якого файлу проєкту може змусити Docker повторно виконати npm ci.
Кращий порядок:
COPY package.json package-lock.json ./
RUN npm ci
COPY src ./src
RUN npm run buildТепер зміни у вихідному коді не скасовують кеш встановлених залежностей.
Фінальний етап повинен містити лише runtime-залежності. Під час проєктування Dockerfile варто перевірити:
чи не копіюється весь каталог проєкту через COPY . .;
чи не потрапляють до production devDependencies;
чи не залишилися вихідні файли, тести та документація;
чи запускається процес без root;
чи присутній тільки потрібний результат складання.
У прикладі команда:
COPY --from=build --chown=node:node /app/dist ./distпереносить тільки скомпільований код і одразу призначає його користувачу node.
Якщо Dockerfile містить:
FROM buildзамість окремого production-етапу, фінальний образ успадкує компілятор, dev-залежності та інші інструменти складання.
Для production потрібно починати новий етап:
FROM node:22-bookworm-slim AS productionpackage-lock.jsonКоманда:
RUN npm ciпотребує lock-файл. Якщо його немає, складання завершиться помилкою.
Створіть його локально:
npm installі додайте до контексту складання.
Ця команда буде помилковою, якщо каталог dist створюється на етапі build, а не на етапі dependencies:
COPY --from=dependencies /app/dist ./distПотрібно копіювати з етапу, на якому каталог справді існує:
COPY --from=build /app/dist ./distКоманда на кшталт:
CMD ["npm", "run", "dev"]може вимагати TypeScript, watcher або інші dev-залежності. У production-етапі вони не встановлюються.
У прикладі запускається вже скомпільований JavaScript:
CMD ["node", "dist/server.js"]node_modulesНе слід переносити локальний node_modules у контейнер:
COPY node_modules ./node_modulesЛокальні модулі можуть бути встановлені для іншої операційної системи або архітектури. Залежності потрібно встановлювати в тому середовищі, де вони запускатимуться.
Не додавайте ключі, паролі чи токени безпосередньо в Dockerfile:
ENV DATABASE_PASSWORD=secret-passwordТаке значення може залишитися в історії образу. Конфігурацію застосунку передають під час запуску контейнера або через механізми керування секретами.
Багатоступеневий Dockerfile розділяє складання і запуск.
Кожен етап має власну інструкцію FROM.
AS задає ім’я етапу.
COPY --from=<stage> переносить файли з іншого етапу.
У фінальному етапі потрібно залишати тільки runtime-файли та production-залежності.
Розділення COPY для lock-файлів і вихідного коду покращує кешування.
npm ci --omit=dev встановлює лише production-залежності.
Запуск від непривілейованого користувача зменшує ризики контейнера.
--target дає змогу створити конкретний етап для перевірки або налагодження.