Образ Docker — неизменяемый шаблон из слоёв: система, рантайм, зависимости и код приложения. Контейнер — запущенный из образа изолированный процесс со своим слоем для записи. Образ описывают в Dockerfile; для Node.js хороший Dockerfile — многоэтапный, с .dockerignore, непривилегированным пользователем и HEALTHCHECK, а данные хранятся в томах, а не внутри контейнера.
После материала вы сможете
- объяснить разницу между образом, контейнером, слоем, реестром и томом
- написать многоэтапный Dockerfile для Node.js-приложения с
.dockerignore,USER nodeиHEALTHCHECKи упорядочить инструкции под кэш сборки - запустить контейнер с портом, переменными окружения и томом и разобраться в нём через
docker ps,logsиexec - объяснить, почему
docker stopможет ждать 10 секунд, и исправить это обработкойSIGTERMили--init
Что знать заранее
Проверено на
Docker Engine 29.6 (Docker Desktop, WSL 2)BuildKit, dockerfile:1Node.js 24.21 (node:24.21.0-alpine, Alpine 3.24)Express 5.3.0TypeScript 7.0.2
Сверено с документацией: 11 октября 2026 г.
Содержание
Образ, контейнер, реестр: что делает Docker
Проблема «у меня работает» возникает из-за разницы окружений: другая версия Node.js, другая ОС, забытая системная библиотека. Docker упаковывает приложение вместе со всем, что ему нужно, и запускает одинаково на ноутбуке, в CI и на сервере. Документация Docker определяет контейнер как изолированный процесс: в отличие от виртуальной машины у него нет своего ядра — все контейнеры на машине используют ядро хоста, поэтому запускаются за доли секунды.
Образ (image)
- Что это
- Неизменяемый набор слоёв: файлы, библиотеки, рантайм, код и команда запуска
- Аналогия
- Установочный диск
Контейнер (container)
- Что это
- Запущенный из образа процесс плюс тонкий слой для записи поверх слоёв образа
- Аналогия
- Установленная и запущенная программа
Dockerfile
- Что это
- Текстовый рецепт сборки образа, по инструкции на слой
- Аналогия
- Исходный код диска
Реестр (registry)
- Что это
- Хранилище образов; по умолчанию — Docker Hub
- Аналогия
- Магазин дисков
Том (volume)
- Что это
- Хранилище данных, которое живёт дольше контейнера
- Аналогия
- Внешний диск
Слой — набор изменений файловой системы: добавленные, изменённые и удалённые файлы. Созданный слой не меняется, а одинаковые слои разные образы используют совместно: образ node:24.21.0-alpine скачивается один раз, сколько бы приложений на нём ни было. Попробуйте запустить Node.js, не устанавливая его:
$ docker run --rm node:24.21.0-alpine node -e "console.log(process.version, process.platform, process.arch)"
v24.21.0 linux x64
$ docker image ls node:24.21.0-alpine
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
node:24.21.0-alpine ebfe2f904627 242MB 62MB --rm удаляет контейнер после выхода.Урок предполагает, что Docker уже установлен (Docker Desktop на Windows и macOS, Docker Engine на Linux), а Node.js-проект вам знаком по уроку «Node.js с нуля». Команды запускались в Git Bash; в PowerShell они те же, кроме time и sleep.
Приложение, которое будем упаковывать
Небольшой API заметок на Express 5 и TypeScript: GET /health для проверки живости, GET и POST /api/notes, заметки сохраняются в JSON-файл в папке DATA_DIR. TypeScript нужен только при сборке — это и есть повод для многоэтапного Dockerfile. Обработчик SIGTERM нужен, чтобы docker stop завершал приложение аккуратно.
{
"name": "notes-api",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/server.js"
},
"dependencies": {
"express": "^5.3.0"
},
"devDependencies": {
"@types/express": "^5.0.6",
"@types/node": "^24.19.2",
"typescript": "^7.0.2"
}
}{
"compilerOptions": {
"target": "es2024",
"module": "nodenext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}src → dist.import express from "express";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
type Note = { id: number; text: string; createdAt: string };
const port = Number(process.env.PORT ?? 3000);
const dataDir = process.env.DATA_DIR ?? "./data";
const file = join(dataDir, "notes.json");
async function loadNotes(): Promise<Note[]> {
try {
return JSON.parse(await readFile(file, "utf8")) as Note[];
} catch {
return [];
}
}
const app = express();
app.use(express.json());
app.get("/health", (_req, res) => {
res.json({ status: "ok", uptime: Math.round(process.uptime()) });
});
app.get("/api/notes", async (_req, res) => {
res.json(await loadNotes());
});
app.post("/api/notes", async (req, res) => {
const text = typeof req.body?.text === "string" ? req.body.text.trim() : "";
if (!text) {
res.status(400).json({ error: "text is required" });
return;
}
const notes = await loadNotes();
const note: Note = { id: notes.length + 1, text, createdAt: new Date().toISOString() };
notes.push(note);
await mkdir(dataDir, { recursive: true });
await writeFile(file, JSON.stringify(notes, null, 2));
res.status(201).json(note);
});
const server = app.listen(port, () => {
console.log(`notes-api listening on port ${port} (NODE_ENV=${process.env.NODE_ENV ?? "development"})`);
});
// docker stop sends SIGTERM: finish open requests and exit instead of waiting to be killed
process.on("SIGTERM", () => {
console.log("SIGTERM received, shutting down");
server.close(() => process.exit(0));
});tsc без ошибок, запуск в Node.js 24.21 и 24.18.npm install express@5.3.0
npm install -D typescript@7.0.2 @types/express@5.0.6 @types/node@24.19.2
npx tsc && node dist/server.jspackage-lock.json нужен для npm ci в образе.Первый Dockerfile и что с ним не так
Начнём с Dockerfile, который пишут в первый раз. Он работает, и на нём хорошо видны типичные ошибки.
FROM node:24.21.0-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]$ docker build -t notes-api:naive -f Dockerfile.naive .
…
#6 [internal] load build context
#6 transferring context: 36.09MB 3.1s done
#7 [3/5] COPY . .
#8 [4/5] RUN npm install
#8 1.800 added 1 package, removed 1 package, and audited 79 packages in 2s
#9 [5/5] RUN npm run build
#10 naming to docker.io/library/notes-api:naive donedocker build --progress=plain (Docker 29.6, BuildKit).$ docker run -d --name notes-naive -p 127.0.0.1:8081:3000 notes-api:naive
a372e6afa938bdfd8a9c6ce1d2e2db88ef4e55460403ea4ae7e78902308e5d6d
$ sleep 2; docker exec notes-naive ps -o pid,user,args
PID USER COMMAND
1 root npm start
18 root {MainThread} node dist/server.js
25 root ps -o pid,user,args
$ time docker stop notes-naive
notes-naive
real 0m0.528s
user 0m0.060s
sys 0m0.077s
$ docker rm notes-naive
notes-naive
$ docker image ls notes-api
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
notes-api:1.0 98bb9f6c383d 247MB 62.3MB
notes-api:naive fcc71eb114bb 354MB 93MB COPY . .без.dockerignoreотправил сборщику 36 МБ — в основномnode_modulesс Windows-машины; npm пришлось заменить пакет со скомпилированным под Windows компилятором TypeScript. Так же в образ попал бы и.envс секретами- Любая правка кода сбрасывает кэш слоя с
COPY . ., иnpm installвыполняется заново при каждой сборке - В образе остались TypeScript и типы — инструменты сборки, которые в работе не нужны: 354 МБ против 247 МБ
- Процессы работают от root; PID 1 —
npm, а не само приложение
Dockerfile по правилам: стадии, .dockerignore, USER, HEALTHCHECK
Сначала .dockerignore — список того, что не уходит в контекст сборки, синтаксис похож на .gitignore. Зависимости и dist собираются внутри образа, а секретам там не место.
# Do not send to the builder: dependencies and build output are created inside the image
node_modules
dist
data
# Secrets and local files never belong in an image
.env
.env.*
.git
npm-debug.log*
Dockerfile*
.dockerignore.gitignore.Теперь многоэтапная сборка: каждая инструкция FROM начинает новую стадию, а COPY --from забирает из предыдущей только нужное. В итоговый образ попадают Node.js, продакшен-зависимости и скомпилированный код — без TypeScript и исходников.
# syntax=docker/dockerfile:1
ARG NODE_VERSION=24.21.0
# 1. Build: all dependencies, including TypeScript, compile src -> dist
FROM node:${NODE_VERSION}-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# 2. Production dependencies only, without devDependencies
FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# 3. Final image: Node.js, production node_modules and compiled code
FROM node:${NODE_VERSION}-alpine AS runtime
ENV NODE_ENV=production \
DATA_DIR=/app/data
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
RUN mkdir -p /app/data && chown node:node /app/data
USER node
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
CMD ["node", "-e", "fetch('http://127.0.0.1:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
CMD ["node", "dist/server.js"]healthy.ARG NODE_VERSION до FROM
- Зачем она здесь
- Версия Node.js в одном месте, закреплена точно: сборка через месяц даст тот же рантайм
FROM … AS build / deps / runtime
- Зачем она здесь
- Три стадии: сборка, продакшен-зависимости, итоговый образ;
--target buildсобирает только первую
COPY package.json package-lock.json до COPY src
- Зачем она здесь
- Слой с
npm ciпересобирается только при смене зависимостей, а не при каждой правке кода
npm ci --omit=dev
- Зачем она здесь
- Точная установка по
package-lock.jsonбез devDependencies
ENV NODE_ENV=production
- Зачем она здесь
- Переменная сохраняется в образе и видна приложению
USER node
- Зачем она здесь
- Пользователь
node(uid 1000) уже есть в официальном образе; процесс работает без прав root
EXPOSE 3000
- Зачем она здесь
- Только документация: порт открывает
-pпри запуске
HEALTHCHECK
- Зачем она здесь
- Docker раз в 10 секунд вызывает
/health; код выхода 0 —healthy, 1 — сбой, после 3 сбоев подряд —unhealthy
CMD ["node", …]
- Зачем она здесь
- Exec-форма: PID 1 — сам Node.js, сигналы доходят до приложения; не
npm start
$ docker build -t notes-api:1.0 .
…
#9 [build 3/7] COPY package.json package-lock.json ./
#10 [build 4/7] RUN npm ci
#11 [deps 4/4] RUN npm ci --omit=dev
#11 0.960 added 65 packages, and audited 66 packages in 693ms
#10 1.585 added 78 packages, and audited 79 packages in 1s
#14 [build 6/7] COPY src ./src
#15 [build 7/7] RUN npm run build
#15 0.261 > notes-api@1.0.0 build
#15 0.261 > tsc
#16 [runtime 4/6] COPY --from=build /app/dist ./dist
#18 [runtime 6/6] RUN mkdir -p /app/data && chown node:node /app/data
#19 naming to docker.io/library/notes-api:1.0 donebuild и deps выполняются параллельно.Слои и кэш сборки
Каждая инструкция, меняющая файлы, даёт слой. При пересборке Docker проверяет, изменились ли инструкция и файлы, которые она копирует: если нет — слой берётся из кэша (CACHED). Как только слой изменился, все слои после него в этой стадии собираются заново. Поэтому редко меняющееся (зависимости) копируют раньше, часто меняющееся (код) — позже. Изменим одну строку в src/server.ts и пересоберём:
$ docker build -t notes-api:1.1 .
…
#10 [build 3/7] COPY package.json package-lock.json ./
#10 CACHED
#9 [build 4/7] RUN npm ci
#9 CACHED
#12 [build 6/7] COPY src ./src
#12 DONE 0.0s
#13 [build 7/7] RUN npm run build
#13 DONE 0.3s
#14 [deps 4/4] RUN npm ci --omit=dev
#14 CACHED
#15 [runtime 3/6] COPY --from=deps /app/node_modules ./node_modules
#15 CACHED
#16 [runtime 4/6] COPY --from=build /app/dist ./dist
#16 DONE 0.0snpm ci в обеих стадиях — из кэша, заново собрались только код и слой dist.$ docker history notes-api:1.0 --format 'table {{.CreatedBy}}\t{{.Size}}' | head -9
CREATED BY SIZE
CMD ["node" "dist/server.js"] 0B
HEALTHCHECK {Test:[CMD node -e fetch('http:/… 0B
EXPOSE [3000/tcp] 0B
USER node 0B
RUN /bin/sh -c mkdir -p /app/data && chown n… 12.3kB
COPY package.json ./ # buildkit 12.3kB
COPY /app/dist ./dist # buildkit 16.4kB
COPY /app/node_modules ./node_modules # buil… 4.44MBnode:24.21.0-alpine.Здесь видно, что из 247 МБ образа своих слоёв — около 4,5 МБ, остальное — базовый образ, который на сервере уже скачан и общий для всех приложений на нём. Документация советует пересобирать образы регулярно (docker build --pull) — образ это снимок, и обновления безопасности базового образа попадут в него только при пересборке.
Запуск: run, ps, logs, exec, stop
Запустим контейнер в фоне (-d), с именем и портом, и пройдём по основным командам.
$ docker run -d --name notes -p 127.0.0.1:8080:3000 notes-api:1.0
14431bf467dc7af7235e5ea4abf38c7864f7775543f5499901030e23ae2c6f06
$ docker ps --filter name=notes
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14431bf467dc notes-api:1.0 "docker-entrypoint.s…" Less than a second ago Up Less than a second (health: starting) 127.0.0.1:8080->3000/tcp notes
$ curl -s http://127.0.0.1:8080/health
{"status":"ok","uptime":0}$ sleep 8; docker ps --filter name=notes
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14431bf467dc notes-api:1.0 "docker-entrypoint.s…" 9 seconds ago Up 8 seconds (healthy) 127.0.0.1:8080->3000/tcp notes
$ docker logs notes
notes-api listening on port 3000 (NODE_ENV=production)
$ curl -s -X POST http://127.0.0.1:8080/api/notes -H "Content-Type: application/json" -d '{"text":"Buy coffee filters"}'
{"id":1,"text":"Buy coffee filters","createdAt":"2026-10-12T01:58:22.446Z"}$ docker exec notes whoami
node
$ docker exec notes ls -la /app
total 28
drwxr-xr-x 1 root root 4096 Oct 12 01:57 .
drwxr-xr-x 1 root root 4096 Oct 12 01:58 ..
drwxr-xr-x 1 node node 4096 Oct 12 01:58 data
drwxr-xr-x 2 root root 4096 Oct 12 01:57 dist
drwxr-xr-x 67 root root 4096 Oct 12 01:57 node_modules
-rwxr-xr-x 1 root root 329 Oct 12 01:56 package.json
$ docker exec notes cat /app/data/notes.json
[
{
"id": 1,
"text": "Buy coffee filters",
"createdAt": "2026-10-12T01:58:22.446Z"
}
]$ docker exec notes ps -o pid,user,args
PID USER COMMAND
1 node {MainThread} node dist/server.js
49 node ps -o pid,user,args
$ docker inspect --format '{{.State.Health.Status}} {{len .State.Health.Log}}' notes
healthy 1
$ time docker stop notes
notes
real 0m0.487s
user 0m0.046s
sys 0m0.091s
$ docker logs notes --tail 2
notes-api listening on port 3000 (NODE_ENV=production)
SIGTERM received, shutting down
$ docker ps -a --filter name=notes
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
14431bf467dc notes-api:1.0 "docker-entrypoint.s…" 11 seconds ago Exited (0) Less than a second ago notes
$ docker rm notes
noteskb-, в статье он убран.docker run -d --name notes -p 127.0.0.1:8080:3000 образ
- Что делает
- Создать и запустить контейнер в фоне
docker ps, docker ps -a
- Что делает
- Работающие контейнеры; с
-a— и остановленные
docker logs notes, docker logs -f notes
- Что делает
- Вывод приложения;
-f— следить в реальном времени
docker exec notes команда, docker exec -it notes sh
- Что делает
- Выполнить команду внутри работающего контейнера;
-it— интерактивная оболочка
docker stop notes
- Что делает
SIGTERM, через 10 секунд —SIGKILL
docker rm notes, docker rm -f notes
- Что делает
- Удалить остановленный контейнер;
-f— остановить и удалить
Почему остановка заняла полсекунды. docker stop отправляет главному процессу SIGTERM и ждёт 10 секунд, затем убивает его SIGKILL. Процесс с PID 1 в Linux особенный: сигналы, для которых нет обработчика, на него не действуют. В документации образа Node.js прямо сказано, что Node.js не рассчитан на роль PID 1. Вот что будет без обработчика:
$ docker run -d --name pid1 node:24.21.0-alpine node -e "setInterval(() => {}, 1000)"
7e0e9b2a5a1331b23b772ba1db38099350be54a98ca08d411a421b600561039b
$ time docker stop pid1
pid1
real 0m10.399s
user 0m0.061s
sys 0m0.091s
$ docker ps -a --filter name=pid1 --format '{{.Names}}: {{.Status}}'
pid1: Exited (137) Less than a second ago
$ docker rm pid1
pid1
$ docker run -d --init --name pid1-init node:24.21.0-alpine node -e "setInterval(() => {}, 1000)"
b334728e597d7e4eabd09cf15e46edc9b3a18b182c21673db9f04a491b9a5ce3
$ time docker stop pid1-init
pid1-init
real 0m0.339s
user 0m0.046s
sys 0m0.091s
$ docker rm pid1-init
pid1-initSIGKILL); с --init — мгновенно.Два решения: обрабатывать SIGTERM в приложении, как в нашем server.ts, или запускать контейнер с --init — тогда PID 1 становится маленький init-процесс, который передаёт сигналы приложению. Обработчик лучше: приложение успевает закрыть соединения и дописать данные.
Порты, переменные окружения и тома
Внутри контейнера своя сеть: приложение слушает порт 3000 в контейнере, а -p ХОСТ:КОНТЕЙНЕР пробрасывает к нему порт машины. Переменные окружения передаются через -e или --env-file. Файлы, записанные внутри контейнера, пропадают вместе с ним — для данных нужен том.
$ docker run -d --name notes -p 127.0.0.1:8080:3000 notes-api:1.0
25d39d3a88a744f5c87357a3f681ab44b8e0a6544b893d30c0fcca8fa6a2370d
$ sleep 1; curl -s -X POST http://127.0.0.1:8080/api/notes -H "Content-Type: application/json" -d '{"text":"Call the supplier"}'
{"id":1,"text":"Call the supplier","createdAt":"2026-10-12T01:59:24.178Z"}
$ docker rm -f notes
notes
$ docker run -d --name notes -p 127.0.0.1:8080:3000 notes-api:1.0
f8605e984fe84d46c12abae2c5663e3f0eba212076956b73d1baa01a9d353f73
$ sleep 1; curl -s http://127.0.0.1:8080/api/notes
[]
$ docker rm -f notes
notes
$ docker volume create notes-data
notes-data
$ docker run -d --name notes -p 127.0.0.1:8080:3000 -v notes-data:/app/data notes-api:1.0
05589d4fff5d56e64bfe16409e085c0bb4b5164a84d0d8f621a751f45613db24
$ sleep 1; curl -s -X POST http://127.0.0.1:8080/api/notes -H "Content-Type: application/json" -d '{"text":"Call the supplier"}'
{"id":1,"text":"Call the supplier","createdAt":"2026-10-12T01:59:28.587Z"}
$ docker rm -f notes
notes
$ docker run -d --name notes -p 127.0.0.1:8080:3000 -v notes-data:/app/data notes-api:1.0
523718889c0c184fd4e10b90cff736bedae3ad6befec681f5f986c0080a087ad
$ sleep 1; curl -s http://127.0.0.1:8080/api/notes
[{"id":1,"text":"Call the supplier","createdAt":"2026-10-12T01:59:28.587Z"}]
$ docker volume ls --filter name=notes-data
DRIVER VOLUME NAME
local notes-data
$ docker volume inspect notes-data --format '{{.Name}} {{.Driver}} {{.Mountpoint}}'
notes-data local /var/lib/docker/volumes/notes-data/_data
$ docker run -d --name notes-2 -p 127.0.0.1:8080:3000 notes-api:1.0
ee5dd4dd6bd165af172b5f0c9d1716414492a49852293f4cfc789585f0665489
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint notes-2 (9e06e8e275da8ac1e377b36926f60ee4c9c68cd70b86c024a7d380d7c640f326): Bind for 127.0.0.1:8080 failed: port is already allocated
Run 'docker run --help' for more information
$ docker rm notes-2
notes-2
$ docker run -d --name notes-public -p 8090:3000 -e NODE_ENV=staging notes-api:1.0
c73d3880d9022ed756df223c9a44be3f1a1ded52075cd780c75ac2ba77687a11
$ docker ps --filter name=notes --format 'table {{.Names}}\t{{.Ports}}'
NAMES PORTS
notes-public 0.0.0.0:8090->3000/tcp, [::]:8090->3000/tcp
notes 127.0.0.1:8080->3000/tcp
$ sleep 1; docker logs notes-public
notes-api listening on port 3000 (NODE_ENV=staging)
$ docker rm -f notes notes-public
notes
notes-publicnotes-data — пережила пересоздание.-p 8090:3000без адреса публикует порт на всех интерфейсах (0.0.0.0и[::]). Документация Docker прямо называет публикацию портов небезопасной по умолчанию: на сервере привязывайте к127.0.0.1, а наружу выпускайте через reverse proxy- Один порт хоста — один контейнер: вторая попытка заняла
127.0.0.1:8080и получилаport is already allocated; созданный контейнер остался в статусеCreated, его пришлось удалить - Именованный том переживает удаление контейнера;
docker run --rmудаляет только анонимные тома - Пустой том при первом подключении заполняется содержимым папки из образа, включая владельца. Поэтому в Dockerfile мы создали
/app/dataи отдали еёnode— иначе приложение без root не сможет писать - Для разработки вместо тома подключают папку с хоста (bind mount,
-v $(pwd)/src:/app/src): удобно, но привязано к файловой системе машины
Частые ошибки и упражнения
port is already allocated
- Причина
- Порт хоста занят другим контейнером или программой
- Что сделать
- Другой порт хоста или остановить того, кто занял; проверить
docker ps
curl: (52) Empty reply или нет ответа при работающем контейнере
- Причина
- Приложение слушает
127.0.0.1внутри контейнера - Что сделать
- Внутри контейнера слушать
0.0.0.0(Express по умолчанию так и делает)
EACCES: permission denied при записи
- Причина
- Папка в образе или томе принадлежит root, а процесс работает от
node - Что сделать
- Создать папку и сделать
chownв Dockerfile доUSER
Сборка каждый раз ставит зависимости заново
- Причина
- Код копируется до
npm ci - Что сделать
- Сначала
package.json,package-lock.jsonиnpm ci, затем код
docker stop ждёт 10 секунд
- Причина
- PID 1 не обрабатывает
SIGTERM - Что сделать
- Обработчик сигнала,
--init, exec-формаCMD
Данные пропали после обновления
- Причина
- Писались в слой контейнера
- Что сделать
- Именованный том для всего, что должно жить дольше контейнера
Следующий шаг — Docker Compose: тот же образ, но вместе с PostgreSQL и Redis, сетью, томами и проверками здоровья в одном файле и одной командой.
Как проверено12 · пройденосверено 11 октября 2026 г.
- запущено, работает
package.json, tsconfig.json, src/server.ts (Express 5.3.0, TypeScript 7.0.2)
npm install во временном проекте, tsc без ошибок, локальный запуск на Node.js 24.18.0: /health, POST/GET /api/notes
- запущено, работает
docker run --rm node:24.21.0-alpine, docker image ls
Docker Engine 29.6.2 (Docker Desktop, WSL 2, хранилище containerd), Git Bash с MSYS_NO_PATHCONV=1
- запущено, работает
Dockerfile.naive: сборка и запуск
контекст 36.09MB, npm install заменил пакет TypeScript под Windows (added 1, removed 1), процессы от root, PID 1 = npm start, 354MB/93MB
- запущено, работает
.dockerignore + многоэтапный Dockerfile (build/deps/runtime, USER node, HEALTHCHECK на fetch)
docker build --progress=plain, образ 247MB/62.3MB, контейнер health: starting → healthy, whoami = node; выдержки вывода сборки — строки из реального лога
- запущено, работает
кэш слоёв после правки src/server.ts, docker history
npm ci в обеих стадиях CACHED, пересобраны build 6-7 и dist; тег 1.1 только для демонстрации, в статье server.ts исходный
- запущено, работает
docker run/ps/logs/exec/inspect/stop/rm
реальный вывод; docker stop 0.49 с, в логе SIGTERM received; в статье префикс kb- у имён контейнеров/образа/тома убран, колонки таблиц выровнены на эти 3 символа
- запущено, работает
PID 1 без обработчика сигнала и с --init
node -e setInterval: docker stop 10.4 с, Exited (137); с --init 0.34 с
- запущено, работает
тома и порты: без тома данные теряются, с именованным томом сохраняются, volume ls/inspect, port is already allocated, -p без адреса = 0.0.0.0 и [::], -e NODE_ENV
реальный вывод, Docker Desktop 29.6.2
- запущено, работает
таблица ошибок: приложение слушает 127.0.0.1 внутри контейнера
node:24.21.0-alpine с listen(3000,'127.0.0.1') и -p 127.0.0.1:8084:3000 → curl: (52) Empty reply from server (Docker Desktop; на Linux-хосте сообщение может отличаться)
- запущено, работает
упражнение modify (без chown)
HTTP 500, в логах EACCES: permission denied, open '/app/data/notes.json', /app/data root:root
- запущено, работает
упражнение apply (финальная стадия alpine:3.24 + бинарник node)
healthy, v24.21.0, npm: not found, 202MB/53.5MB
- запущено, работает
упражнение repeat
шаги совпадают с проверенным прогоном основного сценария
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.