Dive: Как я нашел 800MB мусора в Docker образе и уменьшил его на 45%
Docker образы имеют свойство незаметно распухать. Сегодня расскажу про инструмент, который помог мне найти почти гигабайт лишних файлов в production образе, и покажу, как использовать Dive для оптимизации ваших контейнеров.
История началась с алерта
Утро понедельника началось с сообщения от команды платформы: “Ваши образы занимают 40% места в registry. Можете оптимизировать?”
Проверяю размеры:
docker images | grep my-app
my-app latest 3d4f5g6h 2 hours ago 1.82GB
my-app v1.2.3 1a2b3c4d 1 week ago 1.79GB
my-app v1.2.2 9z8y7x6w 2 weeks ago 1.81GB
Node.js приложение весит почти 2GB? Что-то тут не так.
Первая попытка: docker history
Начал с базового инструмента:
docker history my-app:latest
IMAGE CREATED CREATED BY SIZE
3d4f5g6h 2 hours ago CMD ["node" "dist/index.js"] 0B
<missing> 2 hours ago EXPOSE 3000 0B
<missing> 2 hours ago RUN npm run build 823MB
<missing> 2 hours ago RUN npm ci 645MB
<missing> 2 hours ago COPY . . 12.4MB
<missing> 2 hours ago WORKDIR /app 0B
<missing> 2 hours ago FROM node:18-alpine 352MB
Вижу, что npm ci
и npm run build
занимают много места, но что именно там происходит - непонятно.
Enter Dive
Dive - это инструмент для исследования Docker образов слой за слоем. Он показывает:
- Какие файлы добавлены/изменены/удалены в каждом слое
- Размер каждого файла и директории
- Потраченное впустую место (wasted space)
- Эффективность образа
Установка
# macOS
brew install dive
# Ubuntu/Debian
wget https://github.com/wagoodman/dive/releases/download/v0.12.0/dive_0.12.0_linux_amd64.deb
sudo dpkg -i dive_0.12.0_linux_amd64.deb
# Arch Linux
yay -S dive
# Через Docker (да, иронично)
alias dive="docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive"
Первый запуск
dive my-app:latest
Интерфейс Dive
После запуска видим три основные области:
- Layers (слева сверху) - список всех слоев образа
- Current Layer Contents (справа) - файловая система текущего слоя
- Image Details (снизу) - статистика по образу
Навигация
Tab
- переключение между панелямиCtrl+U/D
- прокрутка списка слоев←→
илиh/l
- навигация по дереву файловSpace
- развернуть/свернуть директориюCtrl+L
- показать только измененные файлы в слоеCtrl+A
- показать добавленные файлыCtrl+R
- показать удаленные файлыCtrl+M
- показать модифицированные файлы
Находка #1: Кеш node-gyp
Переключаюсь на слой RUN npm ci
и вижу:
Current Layer Contents:
├── [+] app/
│ └── [+] node_modules (445 MB)
├── [+] root/
│ ├── [+] .npm (312 MB)
│ └── [+] .node-gyp (156 MB)
└── [+] tmp/
└── [+] node-gyp-1234 (89 MB)
Вот оно! node-gyp (используется для компиляции нативных модулей) оставляет после себя:
- Кеш npm в домашней директории
- Загруженные заголовочные файлы Node.js
- Временные файлы компиляции
Находка #2: Артефакты сборки
В слое RUN npm run build
:
Current Layer Contents:
├── [+] app/
│ ├── [+] dist/ (45 MB) ✓
│ ├── [+] src/ (12 MB) ✗
│ ├── [+] node_modules/
│ │ └── [+] .cache/ (234 MB) ✗
│ ├── [+] .next/ (156 MB) ✗
│ └── [+] coverage/ (78 MB) ✗
Обнаружил:
- Исходники, которые не нужны в production
- Кеш различных build-инструментов
- Результаты тестового покрытия
- Временные файлы Next.js (хотя мы используем обычный Node.js)
Анализ эффективности
Dive показывает метрики эффективности:
Efficiency Score: 42% (Poor)
Wasted Space: 823 MB (45% of image)
Image Size: 1.82 GB
Wasted Space - это файлы, которые были добавлены в одном слое и удалены в другом. Они все равно занимают место в образе.
Оптимизация: Round 1
На основе находок переписал Dockerfile:
FROM node:18-alpine AS builder
WORKDIR /app
# Копируем только необходимое для установки зависимостей
COPY package*.json ./
# Устанавливаем зависимости и сразу чистим кеши
RUN npm ci --only=production && \
npm cache clean --force && \
rm -rf ~/.npm ~/.node-gyp
# Копируем исходники
COPY . .
# Собираем приложение
RUN npm run build && \
rm -rf src/ tests/ .next/ coverage/ && \
find . -name "*.map" -delete && \
find . -name "*.ts" -delete
# Production stage
FROM node:18-alpine
WORKDIR /app
# Копируем только необходимое
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
Результат:
docker build -t my-app:optimized .
docker images | grep my-app
my-app optimized 7h8i9j0k 1 minute ago 623MB
С 1.82GB до 623MB - неплохо!
Оптимизация: Round 2
Запускаю Dive на оптимизированном образе:
dive my-app:optimized
Нахожу еще проблемы:
- В node_modules остались dev-зависимости
- Есть README файлы и лицензии (еще 50MB)
- TypeScript типы (.d.ts файлы)
Продвинутая очистка node_modules
Создал скрипт cleanup-modules.sh
:
#!/bin/sh
# Удаляем dev-зависимости
npm prune --production
# Удаляем ненужные файлы
find node_modules -name "*.md" -delete
find node_modules -name "*.markdown" -delete
find node_modules -name "LICENSE*" -delete
find node_modules -name "*.d.ts" -delete
find node_modules -name "*.js.map" -delete
find node_modules -name ".npmignore" -delete
find node_modules -name ".gitignore" -delete
find node_modules -type d -name "__tests__" -exec rm -rf {} +
find node_modules -type d -name "test" -exec rm -rf {} +
find node_modules -type d -name "tests" -exec rm -rf {} +
find node_modules -type d -name "docs" -exec rm -rf {} +
find node_modules -type d -name ".github" -exec rm -rf {} +
Финальный Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# Устанавливаем только production зависимости
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# Копируем исходники для сборки
COPY . .
RUN npm run build
# Чистим node_modules
COPY cleanup-modules.sh .
RUN chmod +x cleanup-modules.sh && \
./cleanup-modules.sh && \
rm cleanup-modules.sh
# Удаляем все лишнее
RUN rm -rf src/ tests/ .next/ coverage/ \
package-lock.json tsconfig.json \
.eslintrc.js jest.config.js
# Production stage с distroless
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
CMD ["dist/index.js"]
Финальный результат:
docker build -t my-app:final .
dive my-app:final
Efficiency Score: 94% (Excellent)
Wasted Space: 12 MB (3% of image)
Image Size: 387MB
С 1.82GB до 387MB - уменьшили в 4.7 раза!
CI/CD интеграция
Dive можно использовать в CI для контроля размера образов:
# .gitlab-ci.yml
docker-size-check:
stage: test
script:
- docker build -t $CI_PROJECT_NAME:$CI_COMMIT_SHA .
- |
CI=true dive $CI_PROJECT_NAME:$CI_COMMIT_SHA --ci-config .dive-ci.yaml
Файл .dive-ci.yaml
:
rules:
# Образ не должен быть больше 500MB
highestUserWastedPercent: 0.10
highestWastedBytes: 50000000
# Эффективность должна быть выше 90%
lowestEfficiency: 0.90
Практические советы
1. Ищите типичные проблемы
# В Dive нажмите Ctrl+F для поиска
.git/ # Забыли добавить в .dockerignore
.env # Секреты в образе!
node_modules/ # В multi-stage должны копироваться выборочно
*.log # Логи не нужны в образе
.cache/ # Различные кеши
2. Проверяйте историю команд
Часто в образах остается:
/root/.bash_history
/root/.ash_history
/home/user/.zsh_history
3. Анализируйте крупные файлы
В Dive файлы сортируются по размеру. Ищите:
- Дампы баз данных
- Большие JSON/XML файлы
- Архивы и бэкапы
- Медиафайлы
4. Multi-stage паттерны
# Плохо - все зависимости в финальном образе
FROM node:18
RUN npm install
RUN npm run build
# Хорошо - только необходимое
FROM node:18 AS builder
RUN npm install
RUN npm run build
FROM node:18-alpine
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
Альтернативы и дополнения
container-diff
Google’овский инструмент для сравнения образов:
container-diff diff image1 image2 --type=file
docker-slim
Автоматическая оптимизация образов:
docker-slim build --target my-app:latest
hadolint
Линтер для Dockerfile:
hadolint Dockerfile
Чек-лист оптимизации
- Используете multi-stage сборку?
- Очищаете кеши пакетных менеджеров?
- Удаляете временные файлы в том же слое?
- Минимизируете количество слоев?
- Используете .dockerignore?
- Проверили на наличие секретов?
- Удалили исходники после сборки?
- Рассмотрели distroless/alpine образы?
Результаты в production
После оптимизации всех наших образов:
- Registry освободилось на 65%
- Деплой стал быстрее в 3 раза
- Уменьшилось время холодного старта в k8s
- Снизились расходы на трафик
Заключение
Dive - это must-have инструмент для работы с Docker. Он помогает понять, что происходит внутри образа и найти возможности для оптимизации.
В моем случае проблема была в непонимании того, как работает node-gyp и какие файлы он оставляет после себя. Dive сделал эту проблему видимой и помог уменьшить образ почти в 5 раз.
Рекомендую добавить проверку размера образов в ваш CI/CD pipeline и регулярно анализировать их с помощью Dive. Это поможет держать инфраструктуру в порядке и экономить ресурсы.