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 аргумент |
Слои и кэш
Каждая инструкция 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 уже закэширован
.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 (только аргумент заменён)
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"]
Best practices: чеклист
- Один процесс - один контейнер (не засовывай nginx + node + postgres в один образ)
- Сначала зависимости, потом код (кэш слоёв)
.dockerignore- исключай node_modules, .git, .env- Минимальный базовый образ - alpine вместо ubuntu (5 МБ vs 70 МБ)
- Non-root USER - безопасность
- Exec-форма CMD -
CMD ["npm", "start"], неCMD npm start - 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 и сравни время сборки.