Реестр контейнерных образов без настроенных политик очистки и продуманной структуры сборок неизбежно превращается в свалку гигабайтов устаревших артефактов. Типичный продакшен-реестр среднего размера может содержать до 60% неиспользуемых данных: старые билды, дублирующиеся слои и образы без тегов. Результат - перерасход дискового пространства, замедление CI/CD-пайплайнов и лишние расходы на облачный egress-трафик.
В этом руководстве разобраны три ключевых направления оптимизации: сокращение размера образов через многоэтапную сборку и выбор минимального базового образа, настройка автоматической очистки мусора в Docker Registry и Harbor, внедрение стратегии тегирования, исключающей хаос. Каждый метод подкреплён конкретными командами, конфигурациями и цифрами экономии, которые вы сможете применить сразу после прочтения.
Почему ваш реестр пухнет: диагностика и анализ использования диска
Рост реестра происходит по трём причинам. Первая - накопление идентичных слоёв от разных сборок, когда разработчики не используют кэширование и пересобирают образы с нуля. Вторая - отсутствие политик хранения: каждый новый пуш добавляет тег, старые не удаляются. Третья - бесконтрольное использование тега latest, при котором образ перезаписывается, а старый манифест остаётся в хранилище как мусор.
Без регулярной очистки реестр из 100 ГБ может содержать 30-40 ГБ неиспользуемых данных уже через три месяца активной разработки. Диагностика начинается с аудита текущего состояния.
Инструменты для аудита: от docker system df до скриптов
Для локальной оценки использования диска Docker-демоном выполните команду:
docker system df -v
Вывод покажет размер образов, контейнеров, томов и кэша сборки. Флаг -v добавляет детализацию по каждому образу - видно, сколько места занимают конкретные слои и какие из них расшарены между образами.
Для анализа удалённого Docker Registry используйте Catalog API. Получите список всех репозиториев:
curl -s https://registry.example.com/v2/_catalog
Затем для каждого репозитория запросите список тегов:
curl -s https://registry.example.com/v2/myapp/tags/list
Размер конкретного манифеста извлекается через HEAD-запрос к манифесту и анализ заголовка Content-Length. Скрипт для подсчёта суммарного объёма всех образов в registry может выглядеть так:
#!/bin/bash
REGISTRY="https://registry.example.com"
TOTAL_SIZE=0
REPOS=$(curl -s "$REGISTRY/v2/_catalog" | jq -r '.repositories[]')
for repo in $REPOS; do
TAGS=$(curl -s "$REGISTRY/v2/$repo/tags/list" | jq -r '.tags[]?')
for tag in $TAGS; do
SIZE=$(curl -sI "$REGISTRY/v2/$repo/manifests/$tag" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
| grep -i content-length | awk '{print $2}' | tr -d '\r')
TOTAL_SIZE=$((TOTAL_SIZE + SIZE))
done
done
echo "Total size: $((TOTAL_SIZE / 1024 / 1024)) MB"
В Harbor диагностика встроена в административную панель. Раздел Administration → Garbage Collection показывает объём освобождаемого места после последней сборки мусора. На вкладке Projects для каждого проекта отображается количество артефактов и занимаемый объём. Детальный разбор архитектуры слоёв и поиск «тяжёлых» образов удобно выполнять утилитой dive - она показывает каждый слой в интерактивном режиме и подсвечивает неэффективно использованное пространство. Подробнее об анализе слоёв с dive и docker inspect читайте в руководстве по архитектуре Docker-образов.
Стратегия уменьшения размера образов: от Dockerfile до production
Сокращение размера образа - прямой путь к ускорению пайплайнов и снижению нагрузки на реестр. Образ весом 1.2 ГБ выкачивается из реестра за 40-60 секунд при гигабитном канале, а его 150-мегабайтный аналог - за 5-7 секунд. В масштабе сотни деплоев в день разница превращается в часы простоя и гигабайты лишнего трафика.
Многоэтапная сборка: пошаговый разбор и лучшие практики
Multi-stage build решает проблему артефактов сборки, которые не нужны в рантайме. Dockerfile делится на этапы: первый компилирует код и собирает зависимости, второй копирует только готовый бинарник или минимальный набор файлов.
Пример для Go-приложения:
# Этап сборки
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# Этап выполнения
FROM alpine:3.20
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server /usr/local/bin/server
USER 1000
ENTRYPOINT ["server"]
Финальный образ содержит только бинарник и сертификаты - около 15 МБ вместо 800+ МБ полного образа golang с исходниками и тулчейном. Именование этапов через AS builder улучшает читаемость, когда этапов больше двух.
Для интерпретируемых языков многоэтапная сборка тоже эффективна. Пример для Node.js:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY src/ ./src/
USER node
CMD ["node", "src/index.js"]
Здесь на первом этапе устанавливаются только production-зависимости, на втором - копируются модули и исходный код. devDependencies и npm-кэш в образ не попадают. Готовые шаблоны Dockerfile для Python, Node.js и Go с multi-stage сборкой и настройками безопасности собраны в подборке проверенных Dockerfile 2026.
Выбор базового образа: alpine, slim или distroless?
Базовый образ определяет нижнюю границу размера. Сравнение для типового приложения:
| Базовый образ | Размер | Особенности |
|---|---|---|
| ubuntu:22.04 | ~77 МБ | Полный дистрибутив, glibc, apt, shell |
| debian:bookworm-slim | ~28 МБ | Урезанный Debian, glibc, shell |
| alpine:3.20 | ~3 МБ | musl libc, ash, apk |
| gcr.io/distroless/static-debian12 | ~2 МБ | Только рантайм, нет shell, нет пакетного менеджера |
Выбор зависит от сценария. Alpine даёт минимальный размер при наличии shell для отладки, но использует musl libc - это источник трудноуловимых багов с C-расширениями Python или Node.js, собранными под glibc. Slim-образы Debian сохраняют совместимость с glibc и весят вдвое меньше полных версий. Distroless от Google содержит только runtime-зависимости и ваш код - максимальная безопасность и минимальный размер, но отсутствие shell усложняет отладку.
Рекомендация для production: начните с distroless, если приложение не требует shell-доступа. Для отладки держите отдельный debug-образ на базе alpine с установленным curl и busybox. Подробнее о стратегиях очистки неиспользуемых образов в production-окружениях читайте в руководстве по управлению Docker-образами в продакшене.
Настройка автоматической очистки: политики хранения и garbage collection
Ручная очистка реестра нежизнеспособна при активной разработке. Автоматизация решает проблему на двух уровнях: политики хранения определяют, какие образы удалять, сборка мусора физически освобождает место на диске. Без garbage collection удалённый манифест продолжает занимать место в хранилище - это особенность архитектуры Docker Registry, где удаление лишь разрывает ссылку, но не трогает blob-файлы.
Политики хранения в Harbor: настройка без ошибок
Harbor предоставляет гибкий механизм retention policies на уровне проекта. Настройка выполняется в веб-интерфейсе: откройте проект, перейдите на вкладку Policy, создайте новое правило.
Ключевые параметры правила:
- Scope - фильтр по имени репозитория или тегам. Например,
feature/**для всех feature-веток илиrelease-*для релизных сборок. - Retention rules - сколько артефактов оставлять. Можно указать «хранить 5 последних образов» или «удалять образы, не скачивавшиеся 14 дней».
- Schedule - расписание выполнения: cron-выражение или ежедневно/еженедельно.
Рабочий пример для команды из 10 разработчиков:
- Для тега
main-*хранить последние 10 сборок - покрывает откат на неделю назад. - Для тегов
feature/*удалять образы старше 7 дней - feature-ветки живут недолго. - Для тега
latestхранить 3 версии - защита от случайной перезаписи.
Перед включением политики проверьте список кандидатов на удаление: в интерфейсе Harbor есть кнопка Dry Run, которая показывает, какие артефакты попадут под правило, но не удаляет их.
Очистка в Docker Registry: скрипты и ручное управление
Стандартный Docker Registry не имеет встроенных политик хранения. Процесс очистки состоит из трёх шагов: получить список тегов через API, удалить манифест, запустить garbage collection.
Удаление манифеста выполняется запросом:
curl -X DELETE https://registry.example.com/v2/myapp/manifests/sha256:abc123... \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json"
Для получения digest нужного тега сначала запросите манифест с заголовком Accept, в ответе придёт заголовок Docker-Content-Digest.
После удаления манифестов запустите сборку мусора внутри контейнера registry:
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
Эта операция требует перевода registry в read-only режим или полной остановки - garbage collection блокирует хранилище на время работы. Для реестра объёмом 50 ГБ процесс занимает 2-5 минут.
Готовый скрипт для удаления образов старше 30 дней:
#!/bin/bash
REGISTRY="https://registry.example.com"
CUTOFF_DATE=$(date -d '30 days ago' +%s)
REPOS=$(curl -s "$REGISTRY/v2/_catalog" | jq -r '.repositories[]')
for repo in $REPOS; do
TAGS=$(curl -s "$REGISTRY/v2/$repo/tags/list" | jq -r '.tags[]?')
for tag in $TAGS; do
# Пропускаем latest и release-теги
if [[ "$tag" == "latest" || "$tag" == release-* ]]; then continue; fi
CONFIG_DIGEST=$(curl -sI "$REGISTRY/v2/$repo/manifests/$tag" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
| grep docker-content-digest | awk '{print $2}' | tr -d '\r')
CREATED=$(curl -s "$REGISTRY/v2/$repo/blobs/$CONFIG_DIGEST" | jq -r '.created')
CREATED_TS=$(date -d "$CREATED" +%s)
if [ $CREATED_TS -lt $CUTOFF_DATE ]; then
curl -X DELETE "$REGISTRY/v2/$repo/manifests/$CONFIG_DIGEST"
echo "Deleted $repo:$tag"
fi
done
done
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
Скрипт пропускает теги latest и release-*, чтобы не затронуть production-образы. Перед первым запуском сделайте бэкап директории хранения registry - обычно это /var/lib/registry внутри контейнера. Комплексный подход к очистке Docker-окружения, включая удаление контейнеров и томов, описан в инструкции по полной очистке Docker.
Эффективное тегирование: как перестать плодить мусор
Тег latest - главный источник проблем. Он не указывает на конкретную версию, перезаписывается при каждом пуше и делает невозможным откат. В production-окружении образ должен идентифицироваться однозначно.
Рабочая стратегия тегирования для CI/CD:
- Семантическое версионирование:
1.2.3,1.2.3-hotfix1. Подходит для релизных сборок, позволяет быстро найти нужную версию. - Git-коммит:
main-abc1234,feature-login-def5678. Каждый образ привязан к конкретному коммиту - полная воспроизводимость. - Окружение:
staging,prod. Плавающие теги, которые всегда указывают на актуальный образ в окружении. Используйте только для окружений, где допустима автоматическая смена версии.
Для production-деплоев комбинируйте подходы: деплойте по иммутабельному тегу (git-коммит или версия), а тег prod обновляйте атомарно после успешного деплоя. Это даёт возможность мгновенного отката - достаточно переключить prod на предыдущую версию.
Политики очистки напрямую завязаны на теги. Правило «хранить 5 последних образов с тегом release-*» работает только при последовательном тегировании. Если все образы помечены как latest, политика хранения бесполезна - она не сможет отличить вчерашний билд от позавчерашнего.
Ускорение развертывания: как размер образа влияет на CI/CD
Время выкачивания образа из реестра - прямой множитель длительности деплоя. Образ размером 1 ГБ при канале 100 Мбит/с загружается 80 секунд. Тот же образ, оптимизированный до 100 МБ - 8 секунд. При 50 деплоях в день экономия составляет час чистого времени ожидания.
Кэширование слоёв в Docker работает на уровне отдельных инструкций Dockerfile. Слой пересобирается, только если изменилась инструкция или файлы, которые она копирует. Правильный порядок инструкций в Dockerfile ускоряет сборку:
# Часто меняющиеся файлы - в конец
COPY package.json package-lock.json ./
RUN npm ci
COPY src/ ./src/ # Исходный код меняется чаще, чем зависимости
При таком порядке слой с npm ci пересобирается только при изменении lock-файла, а не при каждом изменении исходников. В облачных средах каждый гигабайт egress-трафика из реестра тарифицируется. Для AWS ECR это $0.09/ГБ после первого гигабайта в месяц. Реестр, генерирующий 500 ГБ исходящего трафика, экономит $45/месяц при сокращении размера образов вдвое.
Профилактика и мониторинг: чтобы реестр не пух снова
Оптимизация - не разовая акция. Реестр требует регулярного обслуживания, встроенного в процессы команды.
Настройте мониторинг свободного места. Для Harbor метрики доступны через Prometheus-эндпоинт /metrics. Ключевые метрики: harbor_project_quota_usage_byte - использование квот по проектам, harbor_disk_free - свободное место на диске. Настройте алерт в Grafana при заполнении диска выше 80%.
Встройте проверку размера образа в CI-пайплайн. Добавьте шаг после сборки:
IMAGE_SIZE=$(docker image inspect myapp:latest --format='{{.Size}}')
MAX_SIZE=524288000 # 500 MB in bytes
if [ $IMAGE_SIZE -gt $MAX_SIZE ]; then
echo "Image size $IMAGE_SIZE exceeds limit $MAX_SIZE"
exit 1
fi
Это предотвращает попадание раздутых образов в реестр. Порог в 500 МБ разумен для большинства приложений - если образ превышает лимит, разработчик должен обосновать исключение или оптимизировать Dockerfile.
Проводите ежемесячный аудит с помощью скрипта из первого раздела. Сравнивайте динамику: если объём реестра растёт быстрее, чем количество сервисов, политики очистки требуют пересмотра. Проблема утечек дискового пространства не ограничивается Docker-образами - логи, снапшоты и временные файлы тоже требуют внимания. Системный подход к диагностике описан в руководстве по борьбе с утечками дискового пространства.
Формируйте культуру работы с образами в команде: договоритесь о максимальном размере образа, зафиксируйте соглашение по тегированию в CONTRIBUTING.md репозитория, назначьте ответственного за ежемесячный аудит реестра. Эти практики окупаются стабильностью пайплайнов и предсказуемыми расходами на хранение.