Dockerfile: собираем образ по шагам

Dockerfile: собираем образ по шагам

Dockerfile - это рецепт сборки образа. Каждая строка - инструкция, каждая инструкция - новый слой. Понимание слоёв и кэша - ключ к быстрым сборкам.

Анатомия Dockerfile

FROM node:20-alpine          # базовый образ
WORKDIR /app                 # рабочая директория
COPY package.json package-lock.json ./   # копируем зависимости
RUN npm ci                   # устанавливаем зависимости
COPY . .                     # копируем остальной код
EXPOSE 5173                  # документируем порт
CMD ["npm", "run", "dev", "--", "--host"]  # команда запуска

Основные инструкции

ИнструкцияКогда выполняетсяЧто делает
FROMСборкаЗадаёт базовый образ
WORKDIRСборкаУстанавливает рабочую директорию
COPYСборкаКопирует файлы с хоста в образ
RUNСборкаВыполняет команду (создаёт слой)
CMDЗапускКоманда по умолчанию при docker run
ENTRYPOINTЗапускФиксированная команда (CMD - аргументы)
EXPOSEДокументацияУказывает порт (не пробрасывает!)
ENVСборка + ЗапускУстанавливает переменную окружения
ARGТолько сборкаBuild-time аргумент
`EXPOSE 8080` - это только документация для человека. Чтобы реально пробросить порт, нужен `-p 8080:8080` при `docker run`. Без `-p` порт снаружи недоступен.

Слои и кэш

Dockerfile превращается в стек слоёв: каждая инструкция = слой, контейнер добавляет writable layer сверху

Каждая инструкция FROM, COPY, RUN создаёт слой. Docker кэширует слои и переиспользует их при повторной сборке - пока входные данные не изменились.

# Плохо: любое изменение кода сбрасывает кэш npm ci
FROM node:20-alpine
WORKDIR /app
COPY . .              # <-- тут кэш слетает при любом изменении
RUN npm ci            # переустановка зависимостей каждый раз

# Хорошо: зависимости кэшируются отдельно от кода
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./   # меняется редко
RUN npm ci                                # кэшируется!
COPY . .                                  # меняется часто - но npm ci уже закэширован
Ставь то, что меняется **редко**, в начало Dockerfile (зависимости), а то, что меняется **часто** (код) - в конец. Тогда Docker переиспользует верхние слои.

Правильный порядок инструкций ускоряет повторную сборку в 10+ раз

.dockerignore

Файл .dockerignore работает как .gitignore - исключает файлы из контекста сборки. Без него COPY . . отправит в Docker всё, включая node_modules, .git, .env:

# .dockerignore
node_modules
.git
.env
.env.*
dist
*.log
.DS_Store

Зачем:

  • Скорость - Docker не копирует гигабайты node_modules
  • Безопасность - .env с секретами не попадает в образ
  • Кэш - изменения в .git не инвалидируют кэш COPY . .

COPY vs ADD

<ComparisonTable title="COPY vs ADD" headers={["", "COPY", "ADD"]} rows={[ ["Копирует файлы", "Да", "Да"], ["Распаковывает tar.gz", "Нет", "Да (автоматически)"], ["Скачивает по URL", "Нет", "Да"], ["Предсказуемость", "Высокая", "Низкая (неявное поведение)"], ["Рекомендация", "Используй по умолчанию", "Только если нужна распаковка"] ]} />

В 99% случаев используй COPY. ADD - только если нужно распаковать архив в одну инструкцию.

CMD vs ENTRYPOINT

# CMD - команда по умолчанию, легко заменить при run
CMD ["npm", "start"]
# docker run myapp             → npm start
# docker run myapp npm test    → npm test (CMD заменён)

# ENTRYPOINT - фиксированная команда, CMD добавляет аргументы
ENTRYPOINT ["npm"]
CMD ["start"]
# docker run myapp             → npm start
# docker run myapp test        → npm test (только аргумент заменён)
Всегда используй exec-форму (массив): `CMD ["npm", "start"]`. Shell-форма `CMD npm start` запускает команду через `/bin/sh -c`, что создаёт лишний процесс и мешает graceful shutdown (SIGTERM не доходит до приложения).

Non-root пользователь

По умолчанию процесс в контейнере работает от root. Это небезопасно - если злоумышленник эксплуатирует уязвимость, он получит root:

FROM node:20-alpine
WORKDIR /app

# Создаём пользователя
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

# Переключаемся на non-root
USER appuser

CMD ["npm", "start"]
Переключай `USER` после `COPY` и `RUN`, которым нужны права записи. Если `npm ci` запущен от `appuser`, могут быть проблемы с правами на `node_modules`.

Best practices: чеклист

  1. Один процесс - один контейнер (не засовывай nginx + node + postgres в один образ)
  2. Сначала зависимости, потом код (кэш слоёв)
  3. .dockerignore - исключай node_modules, .git, .env
  4. Минимальный базовый образ - alpine вместо ubuntu (5 МБ vs 70 МБ)
  5. Non-root USER - безопасность
  6. Exec-форма CMD - CMD ["npm", "start"], не CMD npm start
  7. COPY вместо ADD - предсказуемость

Мини-задание

  • Создай Dockerfile для простого Node-приложения (express hello world)
  • Добавь .dockerignore с node_modules, .git, .env
  • Собери: docker build -t myapp .
  • Запусти: docker run --rm -p 3000:3000 myapp
  • Проверь размер образа: docker images myapp
  • Добавь USER - пересобери и проверь: docker exec -it <id> whoami

Итог

  • Dockerfile - рецепт: каждая инструкция создаёт слой, слои кэшируются.
  • Порядок инструкций определяет эффективность кэша: зависимости → код.
  • .dockerignore = безопасность + скорость + стабильный кэш.
  • COPY > ADD, exec-форма CMD > shell-форма, non-root USER > root.
  • Один контейнер = один процесс. Не мешай сервисы в одном образе.

Типичная ошибка

Самая частая - COPY . . перед npm ci/go mod download. Результат: любое изменение в коде (даже пробел в README) сбрасывает кэш зависимостей. Сборка из 10 секунд превращается в 2 минуты.

Вторая ошибка - забыть .dockerignore. Без него COPY . . копирует node_modules (200+ МБ), .git (весь репозиторий) и .env (секреты прямо в образе).

Мини-практика (10-15 минут)

Проверь влияние порядка инструкций на кэш:

# 1. Создай простое приложение с package.json
mkdir /tmp/docker-test && cd /tmp/docker-test
echo '{"name":"test","scripts":{"start":"echo ok"}}' > package.json
echo 'console.log("hello")' > index.js

# 2. Напиши Dockerfile (правильный порядок)
cat > Dockerfile <<'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]
EOF

# 3. Собери дважды - второй раз мгновенно (кэш)
docker build -t test1 .
docker build -t test1 .   # --> Using cache

# 4. Измени index.js и собери - npm install закэширован
echo 'console.log("changed")' > index.js
docker build -t test1 .   # npm install из кэша!

Поэкспериментируй: поставь COPY . . перед RUN npm install и сравни время сборки.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.