Docker Hub ограничил доступ: свой registry как страховка сборок
Сборка падает на docker pull, в логах CI — toomanyrequests: You have reached your pull rate limit, а иногда образ просто не тянется с registry-1.docker.io вообще, потому что сеть до Docker Hub из России нестабильна. Проблема не в вашем Dockerfile и не в CI-раннере — вы зависите от чужого сервиса, лимиты и доступность которого вы не контролируете. Решение прямое: поднять собственный Docker Registry с кэшированием образов из Docker Hub на своём сервере, и переключить на него сборки и серверы развёртывания. Разберём, как это сделать так, чтобы сборки перестали быть заложником чужой политики доступа.
Содержание
Почему Docker Hub стал ненадёжной точкой опоры
Есть два разных сценария, и их стоит различать, потому что решаются они одинаково, но с разным приоритетом.
Первый — лимиты. У Docker Hub есть ограничения на число pull-запросов в единицу времени для анонимных и бесплатных аккаунтов, и эти лимиты периодически пересматриваются не в сторону смягчения. Точные цифры называть не буду — они меняются, и на момент чтения этой статьи у вас могут быть другие значения, проверяйте актуальные в документации Docker. Важнее сам факт: если у вас десяток CI-раннеров, которые параллельно тянут node:20-alpine или postgres:16 при каждой сборке, вы упираетесь в лимит быстрее, чем кажется, — считает Docker Hub не по проекту, а по IP или по аккаунту, и все ваши раннеры на одном сервере бьют в один и тот же счётчик.
Второй сценарий — нестабильность или полная недоступность Docker Hub из конкретной сети. Для инфраструктуры в России это отдельная головная боль: соединение до registry-1.docker.io и auth.docker.io может рваться, тормозить или не устанавливаться вовсе в зависимости от провайдера и момента времени, и предсказать это заранее нельзя. Сборка, которая вчера отработала за две минуты, сегодня падает по таймауту на первом же FROM.
Оба сценария лечит одно архитектурное решение: между вашими серверами и Docker Hub встаёт собственный registry на сервере, к которому у вас стабильный доступ. Дальше ваши CI-раннеры и продакшн-серверы обращаются только к нему, а он уже сам решает вопрос с апстримом — либо отдаёт закэшированный образ, либо один раз сходит за ним в Docker Hub. Если этот registry стоит на сервере с чистым зарубежным IP (например, в UK), то соединение с Docker Hub идёт из другой сети — а внутренняя инфраструктура в России общается только с вашим собственным сервером по обычному HTTPS, который не зависит от того, как в моменте ведёт себя маршрут до docker.io.
Свой Docker Registry: минимальная установка под кэш
Базовая установка приватного registry — с HTTPS через реверс-прокси, авторизацией через htpasswd и push/pull своих образов — подробно разобрана в отдельном материале: как установить и настроить приватный Docker Registry на VPS. Если вам нужен и приватный реестр под свои образы, и кэш для Docker Hub одновременно — читайте оба материала вместе, но с одним важным уточнением: режим прокси-кэша и режим обычного реестра для push своих образов не совмещаются в одном инстансе registry:2.
Официальный образ registry:2 умеет работать в двух режимах на выбор: обычный реестр (принимает push, хранит то, что вы туда залили) или прокси-кэш (только читает из апстрима, ничего не принимает через push). Это одна из первых граблей: если вы включили proxy.remoteurl, docker push в этот же адрес просто не сработает. На практике держат два отдельных инстанса — один под свои образы, другой под кэш Docker Hub, с разными портами и, как правило, разными поддоменами.
Минимальный запуск под кэш выглядит так:
mkdir -p /opt/registry-cache/data
docker run -d \
--name registry-cache \
--restart=unless-stopped \
-p 127.0.0.1:5001:5000 \
-v /opt/registry-cache/data:/var/lib/registry \
-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
-e REGISTRY_PROXY_TTL=168h \
registry:2
Как и в базовой установке, порт привязан к 127.0.0.1 — наружу отдавать нужно через реверс-прокси с HTTPS, потому что Docker принципиально отказывается работать с реестром по обычному HTTP за пределами localhost. Домен под кэш заводите отдельный от домена под приватный реестр, например docker-cache.example.com, и настраивайте на него тот же Nginx-конфиг с client_max_body_size 0 и сертификатом Let's Encrypt, что и для обычного registry.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверPull-through cache: как кэш реально работает
Логика прокси-кэша простая: при первом запросе конкретного образа и тега registry идёт в Docker Hub, скачивает слои, отдаёт их клиенту и одновременно сохраняет у себя. При повторном запросе того же тега registry сверяет манифест с апстримом (это лёгкий запрос, не полное скачивание) и, если ничего не изменилось, отдаёт слои из своего локального хранилища, не гоняя их заново через внешнюю сеть.
Здесь есть нюанс, который часто понимают неправильно: пул-through cache не отменяет обращение к Docker Hub полностью. Проверка манифеста на актуальность всё равно идёт наружу при каждом pull — просто она лёгкая и не тянет за собой мегабайты слоёв. Это значит, что кэш не решает проблему полной недоступности Docker Hub сам по себе (если сеть до Hub легла совсем, проверка манифеста тоже не пройдёт), но кардинально снижает объём трафика и число «тяжёлых» запросов, которые как раз и упираются в лимиты.
Переменная REGISTRY_PROXY_TTL задаёт время жизни закэшированного слоя — после истечения TTL registry считает копию устаревшей и обновит её при следующем обращении. Значение 168h (неделя) — разумная отправная точка для стабильных базовых образов вроде alpine или конкретных версионных тегов типа node:20.11-alpine; для тегов вида latest, которые обновляются апстримом часто, TTL стоит держать короче, иначе рискуете неделями гонять устаревшую версию под видом свежей.
Отдельное ограничение чистого registry:2 в режиме прокси — один инстанс проксирует только один апстрим-реестр. Нужен кэш и для ghcr.io, и для quay.io — поднимайте отдельный контейнер под каждый апстрим на своём порту и поддомене. Если это неудобно, посмотрите в сторону Harbor или Nexus Repository — оба умеют несколько прокси-конфигураций в одном инстансе, но и ресурсов требуют заметно больше, чем голый registry:2. Готовый Docker Compose под Harbor: Harbor в Docker Compose — оправдан, когда внешних реестров несколько и нужна единая точка входа с UI, ролями и сканированием уязвимостей.
Переключение CI/CD и серверов на свой registry
Просто поднять кэш недостаточно — сборки и деплой должны реально начать через него ходить, а тут есть два подхода с разной степенью надёжности.
Способ 1: registry-mirrors в конфиге Docker. Docker daemon умеет сам подставлять зеркало для образов без явного адреса реестра (то есть для всего, что по умолчанию резолвится в docker.io). На каждом сервере, где стоит Docker — включая CI-раннеры, — правите /etc/docker/daemon.json:
{
"registry-mirrors": ["https://docker-cache.example.com"]
}
systemctl restart docker
Плюс способа — не нужно менять ни один Dockerfile, docker pull nginx и FROM postgres:16 продолжают работать как раньше, просто уходят через ваш кэш. Минус — это работает только для образов из Docker Hub; если в Dockerfile явно указан другой реестр (FROM ghcr.io/...), зеркало его не перехватывает. И второй минус: если ваш кэш временно недоступен, Docker по умолчанию не всегда прозрачно откатывается на прямой Hub — поведение зависит от версии, поэтому не полагайтесь на этот механизм как на единственный путь без мониторинга самого кэша.
Способ 2: явная замена адреса в Dockerfile и compose-файлах. Надёжнее, но требует правки конфигов:
FROM docker-cache.example.com/library/node:20-alpine AS build
...
FROM docker-cache.example.com/library/nginx:1.27-alpine
Обратите внимание на library/ — так в Docker Hub называется неймспейс официальных образов, и при обращении через сторонний registry префикс нужно указывать явно, тогда как при обращении напрямую к Docker Hub вы его не пишете (Docker подставляет сам). Эта деталь — частая причина ошибки manifest unknown при первом переключении.
Для GitLab CI, если раннеры настроены с изоляцией через Docker executor, задайте зеркало на уровне конфига раннера config.toml:
[[runners]]
[runners.docker]
pull_policy = ["always"]
[[runners.docker.mirrors]]
registry = "docker-cache.example.com"
или проще — пропишите зеркало в daemon.json на самом хосте с раннерами (способ 1), это работает без изменений в .gitlab-ci.yml каждого проекта. Подробная настройка GitLab CI/CD на VPS с раннерами разобрана отдельно: GitLab CI/CD на VPS: настройка. Если кэш требует авторизации (рекомендуется, чтобы им не пользовался кто попало), логин на раннерах и продакшн-серверах прописывается один раз:
docker login docker-cache.example.com
и сохраняется в ~/.docker/config.json — для CI это удобнее хранить как секрет и подкладывать перед сборкой, а не логиниться вручную на каждом раннере.
Диск: сколько места закладывать под кэш образов
Кэш растёт монотонно, пока вы не настроите TTL и чистку, и это главная эксплуатационная забота собственного registry. Точную цифру под ваш случай никто не назовёт заранее — она зависит от количества уникальных базовых образов и тегов, которые реально используют ваши проекты, но есть ориентиры, на которые можно опираться при планировании.
Разброс размеров базовых образов большой: минималистичные вроде alpine — единицы мегабайт, node, python или postgres с типовыми зависимостями — уже сотни мегабайт за тег, образы с ML-библиотеками или полным дистрибутивом — легко больше гигабайта. Если у команды в ходу условно 15–20 базовых образов с несколькими тегами каждый и глубокая история слоёв не нужна (TTL их вымывает), для старта разумно закладывать диск не меньше 50–100 ГБ под кэш отдельно от системного — с запасом, а не впритык: забитый под завязку диск роняет и кэш, и весь сервер разом. Общий подход к тому, сколько места закладывать с запасом, разобран отдельно: сколько дискового пространства закладывать с запасом.
Смотрите за фактическим потреблением, а не гадайте заранее:
du -sh /opt/registry-cache/data
docker system df
Если кэш растёт быстрее, чем ожидалось, — либо TTL слишком большой для объёма используемых тегов, либо в проектах гуляет слишком много разных версий одного и того же образа (типичная причина — разработчики не фиксируют версию тега и каждый использует что попало). Второе лечится процессом, а не диском: зафиксируйте в шаблонах Dockerfile конкретные версионные теги для команды.
Мониторинг, обслуживание и грабли
Сам по себе кэш не чистит себя надёжно без внимания — вот на что смотреть регулярно.
Garbage collection. Устаревшие по TTL слои не удаляются с диска автоматически сразу — TTL решает, когда манифест считается протухшим и будет перекачан заново, но старые неиспользуемые блобы копятся до явной сборки мусора:
docker exec registry-cache bin/registry garbage-collect /etc/docker/registry/config.yml
Грабля здесь в том, что запускать GC на «горячем» реестре, который параллельно принимает запросы на запись, небезопасно — можно удалить блоб, на который в этот момент идёт push. Для кэша Docker Hub, который сам ничего не принимает через push от внешних клиентов, риск ниже, чем для обычного реестра, но для порядка временно переводите контейнер в режим только для чтения через maintenance.readonly.enabled: true в конфиге перед прогоном GC, если хотите подстраховаться, и возвращайте обратно после.
Здоровье кэша. Добавьте простую проверку в существующий мониторинг — curl на /v2/ с ожидаемым HTTP 200, и алерт при недоступности. Если кэш падает, а вы полагались на него как на единственный путь до образов (без прямого фолбэка на Docker Hub на серверах развёртывания), сборки встанут все и сразу — то есть кэш сам становится точкой отказа, если про него забыть.
Не забывайте про сам образ registry:2. Ирония в том, что образ, который кэширует Docker Hub для вас, сам тянется откуда-то при обновлении — держите его версию явно зафиксированной тегом (registry:2.8.3, а не голый registry:2 без патч-версии) и обновляйте осознанно, а не молча на пересоздании контейнера.
Бэкап кэша не нужен, бэкап конфига — нужен. Данные в кэше — это чужие публичные образы, их не жалко потерять: контейнер пересоздастся и перекачает всё заново с нуля. А вот конфиг реестра, сертификаты и файл авторизации (если используете) теряться не должны, иначе после сбоя переезд встанет не из-за данных, а из-за забытых настроек.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли использовать один registry и для своих образов, и как кэш Docker Hub?
Нет, режимы взаимоисключающие в одном инстансе registry:2 — прокси-кэш не принимает push. Держите два отдельных контейнера на разных портах или поддоменах, либо переходите на Harbor/Nexus, где оба сценария есть в одном UI.
Полностью ли pull-through cache решает проблему недоступности Docker Hub из России?
Нет полностью — при pull всё равно идёт лёгкий запрос к апстриму на проверку манифеста, и если сеть до Docker Hub легла совсем, эта проверка тоже не пройдёт. Кэш радикально снижает объём и частоту обращений к Hub, но сервер с самим кэшем стоит держать в локации со стабильным доступом наружу, например в UK или US.
Сколько диска нужно под кэш образов?
Точной цифры нет, зависит от числа используемых базовых образов и тегов. Для старта разумно закладывать не меньше 50–100 ГБ отдельно от системного диска, дальше смотреть на фактический рост через du -sh и настраивать TTL под свой объём.
Обязательно ли переписывать все Dockerfile под адрес своего registry?
Нет, если устраивает автоматическое зеркалирование через registry-mirrors в daemon.json — тогда docker pull nginx продолжает работать как раньше. Явная замена адреса в Dockerfile надёжнее и работает предсказуемо при недоступности зеркала, но требует правки конфигов и учёта префикса library/ для официальных образов.
Что будет, если сервер с кэшем упадёт?
Если на серверах развёртывания и раннерах настроен только путь через кэш без фолбэка — сборки встанут. Добавьте мониторинг доступности /v2/ на самом кэше и держите план Б (временный откат registry-mirrors из daemon.json), а не полагайтесь на то, что кэш никогда не ляжет.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →