Минимальный рабочий Dockerfile:
Возьмём небольшой API на Node.js: server.js, package.json и package-lock.json. В корне проекта создаём файл с именем Dockerfile, без расширения:
Код: Выделить всё
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Сборка и контекст:
Код: Выделить всё
docker build -t shop-api:0.1 .
docker run -d -p 3000:3000 --name shop shop-api:0.1
curl http://localhost:3000Код: Выделить всё
node_modules
.git
*.log
.envПочему package.json копируется отдельно:
Каждая инструкция создаёт слой, и Docker кэширует их. Пока файлы, которые берёт COPY, не изменились, слой и всё до следующего изменения подтягиваются из кэша. Зависимости меняются редко, код постоянно. Поэтому сначала копируем только манифесты и ставим пакеты, а весь код заносим после. Правка в server.js пересоберёт лишь последние слои, npm ci отработает из кэша мгновенно. Если же написать COPY . . одной строкой в начале, любое изменение кода будет заново тянуть все зависимости. На CI это разница между сборкой за десять секунд и за три минуты.
CMD и форма записи:
CMD с массивом в квадратных скобках называется exec-формой. Процесс запускается напрямую, получает PID 1 и сигналы вроде SIGTERM. Shell-форма CMD node server.js заворачивает запуск в /bin/sh, и при docker stop приложение может вообще не узнать об остановке: демон подождёт 10 секунд и убьёт контейнер. Корректное завершение так не сделать, пишите exec-форму. Есть ещё ENTRYPOINT: он жёстко фиксирует исполняемую команду, а CMD тогда превращается в аргументы по умолчанию. Для своих приложений на старте хватает одного CMD.
Типичные грабли:
Размазанный apt-get. В образах на Debian или Ubuntu команды update и install обязаны жить в одном RUN, иначе закэшированный слой с update протухнет и install начнёт ставить пакеты по устаревшим индексам:
Код: Выделить всё
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*Запуск от root. По умолчанию процесс в контейнере работает от root. В официальных образах node уже есть непривилегированный пользователь, достаточно строки USER node перед CMD. Подробнее эту тему разберём в главе про безопасность.
Забытый .dockerignore. Симптом: docker build надолго зависает на шаге "transferring context".
Итог:
Вы умеете описывать образ через FROM, WORKDIR, COPY, RUN, EXPOSE и CMD, собирать его с тегом и выстраивать инструкции так, чтобы кэш слоёв работал на вас. Данные внутри контейнера пока живут ровно до его удаления, в следующей главе подключим тома и разберёмся с хранением. А радикально ужимать образы multi-stage сборкой будем в девятой главе.