Пошук уроків, статей та іншого контенту
Оптимізуєте Dockerfile, мінімізуєте поверхню атаки та виключите зайві пакети й файли з образу.
Docker-образ містить усе, що потрібно застосунку для запуску: базову операційну систему, бібліотеки, залежності, файли застосунку та налаштування.
Зайві компоненти збільшують:
розмір образу;
час його завантаження;
кількість потенційних вразливостей;
поверхню атаки — набір доступних програм, файлів і можливостей, якими може скористатися зловмисник.
Безпечний Dockerfile має містити лише те, що справді потрібно для запуску застосунку.
Основні правила:
Використовуйте мінімальний базовий образ.
Не копіюйте в образ зайві файли.
Встановлюйте лише production-залежності.
Використовуйте багатоступеневе збирання, коли застосунку потрібні інструменти лише під час build.
Запускайте процес не від імені root.
Не записуйте секрети в Dockerfile або образ.
Порівняйте два варіанти:
FROM node:22FROM node:22-bookworm-slimПерший образ містить більше системних пакетів і інструментів. Другий — зменшений варіант, у якому залишено менше компонентів.
Для production-образу зазвичай варто використовувати варіант із суфіксом:
slim — зменшений образ на основі Debian;
alpine — дуже малий образ на основі Alpine Linux.
Малий образ не завжди автоматично є найкращим. Деякі залежності очікують наявності системних бібліотек Debian або Ubuntu й можуть працювати з Alpine складніше. Для початку практичним вибором часто є slim.
Версію образу потрібно вказувати явно:
FROM node:22-bookworm-slimНе використовуйте без потреби:
FROM node:latestТег latest може вказувати на різний вміст у різний час. Через це збірки стають непередбачуваними. Для суворого контролю версії образ можна додатково закріпити за digest, але для більшості початкових проєктів достатньо конкретного тегу та регулярного оновлення залежностей.
.dockerignoreDocker надсилає контекст збірки, тобто файли з каталогу проєкту, демону Docker. Якщо не виключити зайві файли, вони можуть:
потрапити в образ;
збільшити контекст збірки;
розкрити локальні секрети;
призвести до непотрібного скидання кешу шарів.
Створіть у корені проєкту файл .dockerignore:
node_modules
npm-debug.log
.git
.gitignore
.env
.env.*
coverage
dist
Dockerfile
.dockerignore
README.mdФайл .env особливо важливий. У ньому часто містяться паролі, токени або ключі доступу.
.dockerignore не є механізмом захисту секретів у вже створеному образі. Якщо секрет потрапив у попередній шар образу, просте видалення файлу в наступному шарі не гарантує його видалення з історії. Тому секрети взагалі не слід копіювати в контекст збірки.
Docker кешує окремі шари. Це можна використати для швидших повторних збірок.
Невдалий порядок:
COPY . .
RUN npm installЗміна будь-якого файлу застосунку змусить Docker повторно виконати встановлення залежностей.
Кращий порядок:
COPY package*.json ./
RUN npm ci --omit=dev
COPY src ./srcТепер зміна файлу в src не змушує повторно встановлювати залежності, якщо package.json і package-lock.json не змінилися.
Для відтворюваного встановлення залежностей Node.js використовуйте:
npm ciКоманда npm ci використовує package-lock.json і призначена для автоматизованих збірок.
Для production-образу не потрібні залежності, які використовуються лише під час розробки:
npm ci --omit=devТак можна виключити, наприклад:
тестові фреймворки;
форматери;
лінтери;
інструменти розробки.
Не використовуйте npm install без lock-файлу в production-збірках, якщо вам важлива повторюваність результату.
За замовчуванням процес у контейнері може запускатися від імені root. Якщо в застосунку виникне вразливість, права root можуть збільшити наслідки атаки.
Офіційний образ Node.js містить користувача node. Його можна вказати в Dockerfile:
USER nodeРозташовуйте цю інструкцію після операцій, яким потрібні підвищені права, наприклад після копіювання файлів і встановлення залежностей.
Перевірити користувача всередині контейнера можна так:
docker run --rm my-node-app idУ результаті має бути користувач node, а не root.
Створимо мінімальний Node.js-застосунок.
Структура проєкту:
.
├── package.json
├── package-lock.json
├── Dockerfile
├── .dockerignore
└── src
└── server.jsФайл package.json:
{
"name": "secure-node-app",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node src/server.js"
},
"dependencies": {
"express": "^4.21.2"
}
}Файл src/server.js:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (_request, response) => {
response.send('Застосунок працює');
});
app.listen(port, '0.0.0.0', () => {
console.log(`Сервер слухає порт ${port}`);
});Створіть lock-файл локально:
npm installФайл Dockerfile:
FROM node:22-bookworm-slim AS dependencies
WORKDIR /app
COPY package.json package-lock.json ./
# Встановлюємо лише залежності для production
RUN npm ci --omit=dev \
&& npm cache clean --force
FROM node:22-bookworm-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY package.json package-lock.json ./
COPY src ./src
# Запускаємо застосунок без прав root
USER node
EXPOSE 3000
CMD ["node", "src/server.js"]Файл .dockerignore:
node_modules
npm-debug.log
.git
.env
.env.*
coverage
dist
Dockerfile
.dockerignore
README.mdЗберіть образ:
docker build -t secure-node-app .Запустіть контейнер:
docker run --rm -p 3000:3000 secure-node-appПісля цього застосунок буде доступний на http://localhost:3000.
Перевірте, від якого користувача він працює:
docker run --rm secure-node-app idУ прикладі:
використано зменшений базовий образ bookworm-slim;
залежності копіюються до файлів застосунку, щоб ефективно використовувати кеш;
встановлюються лише production-залежності;
очищується кеш npm;
у фінальний образ копіюються тільки потрібні файли;
застосунок запускається від імені node;
секрети та локальні файли виключаються через .dockerignore.
Комбінація dependencies і фінального етапу є формою багатоступеневого збирання. Перший етап готує залежності, а другий містить лише результат, необхідний для запуску.
Деякі застосунки спочатку потрібно скомпілювати або зібрати. Наприклад, під час build можуть бути потрібні компілятор, CLI-інструменти та dev-залежності, але в runtime вони не потрібні.
Схема такого Dockerfile:
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev \
&& npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]У фінальний етап не потрапляють:
вихідні файли, якщо вони не потрібні для запуску;
dev-залежності;
інструменти збирання;
проміжні артефакти;
кеш пакетного менеджера.
Назви каталогу dist і команди npm run build залежать від конкретного проєкту. Їх потрібно замінити на фактичні команди та результати збирання вашого застосунку.
Якщо в Dockerfile встановлюються системні пакети через apt-get, очищуйте списки пакетів в одній інструкції:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*Важливо:
apt-get update і apt-get install мають бути в одному шарі;
параметр --no-install-recommends не встановлює необов’язкові рекомендовані пакети;
кеш списків пакетів видаляється після встановлення.
Не додавайте системні пакети «про всяк випадок». Кожен додатковий пакет збільшує розмір образу та кількість компонентів, які потрібно оновлювати.
Не записуйте секрети безпосередньо в Dockerfile:
# Небезпечно
ENV DATABASE_PASSWORD=my-secret-passwordТакож не копіюйте їх у образ:
# Небезпечно
COPY .env .envЗначення конфігурації потрібно передавати під час запуску контейнера відповідним механізмом середовища виконання. При цьому секрет не має потрапляти до Dockerfile, .dockerignore-виключеного контексту або шару образу.
Після збирання перевірте:
чи запускається контейнер від імені не-root користувача;
чи не потрапили в образ .env, .git і node_modules;
чи встановлено лише production-залежності;
чи працює застосунок без доступу до зайвих файлів;
чи не містить Dockerfile паролів і токенів.
Подивитися розмір образу можна командою:
docker images secure-node-appПереглянути шари образу:
docker history secure-node-appdocker history допомагає побачити, які команди створювали шари. Якщо секрет колись був переданий через ENV, ARG або записаний у файл під час build, він може залишитися в історії навіть після подальшого видалення.
latestFROM node:latestТакий тег не гарантує однакову версію образу в усіх збірках. Використовуйте конкретний тег базового образу.
COPY . .Без .dockerignore до контексту можуть потрапити .git, .env, локальні залежності та журнали. Навіть із .dockerignore краще копіювати потрібні каталоги явно.
RUN npm installЦе може додати в образ непотрібні інструменти. Для production використовуйте lock-файл і:
RUN npm ci --omit=devrootЯкщо Dockerfile не містить USER, застосунок часто запускається від імені root. Додавайте USER після підготовки файлів і залежностей.
RUN npm cache clean --forceЯкщо кеш було створено в попередньому шарі, видалення в новому шарі не обов’язково зменшить розмір образу. Встановлення та очищення кешу потрібно об’єднувати в одну інструкцію RUN.
Кожна додаткова утиліта збільшує поверхню атаки. Додавайте лише пакети, які справді потрібні для збирання або запуску застосунку.
Безпечний Docker-образ:
використовує конкретний і мінімальний базовий образ;
не містить локальних файлів, секретів і dev-залежностей;
використовує .dockerignore;
застосовує багатоступеневе збирання, якщо build-інструменти не потрібні під час запуску;
очищує кеші та списки пакетів у тому самому шарі;
запускає застосунок від імені непривілейованого користувача;
містить лише файли й пакети, необхідні для роботи застосунку.