Docker pull тормозит или упирается в лимит: что делать
Медленный docker pull обычно складывается из трёх причин: лимиты реестра, тяжёлые образы и нестабильная сеть. Разберём, как отличить одно от другого и что делать в каждом случае.
Как понять, что именно тормозит
Сначала разделите три случая: отказ реестра, проблема с DNS или соединением, медленная передача. Каждый проверяется одной командой:
time docker pull alpine:latest
docker info | grep -i -A2 'registry mirrors'
curl -s -o /dev/null -w 'connect %{time_connect} tls %{time_appconnect}\n' https://registry-1.docker.io/v2/Маленький образ вроде alpine на нормальном канале скачивается за секунды. Если даже он долго висит до начала загрузки слоёв, ищите проблему в DNS, прокси или соединении с реестром. Если старт быстрый, а прогресс по слоям ползёт, дело в пропускной способности или в размере образа. Отказ по лимиту приходит сразу и с понятным текстом, его ни с чем не спутать.
Как понять, что упёрлись в лимит
Docker Hub ограничивает число скачиваний за период времени. Когда лимит исчерпан, реестр отвечает кодом 429, а docker выводит:
Error response from daemon: toomanyrequests: You have reached your pull rate limit.
You may increase the limit by authenticating and upgradingВ других версиях текст может отличаться, например You have reached your unauthenticated pull rate limit, но ключевое слово всегда toomanyrequests. Это не сбой сети: реестр отказал намеренно, и повтор через минуту не поможет.
Остаток лимита можно узнать, не тратя его. Docker описывает в документации такой способ: получить анонимный токен и отправить запрос HEAD, который в лимит не засчитывается.
TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token)
curl -s --head -H "Authorization: Bearer $TOKEN" \
https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest | grep -i ratelimitВ ответе будут заголовки ratelimit-limit, ratelimit-remaining и docker-ratelimit-source. Последний показывает, по какому адресу или аккаунту вас считают. Если заголовков нет, для вашего аккаунта или адреса лимит не действует.
Как устроен лимит
- Считаются запросы манифеста образа, а не отдельные слои. Для образа под несколько архитектур каждая скачанная архитектура считается отдельно.
- Анонимные запросы считаются по IPv4-адресу или по подсети IPv6 /64. Всё, что выходит в интернет через один адрес, делит общий лимит: офис, CI-раннеры за NAT, общий хостинг.
- Для авторизованных пользователей лимит выше и считается по аккаунту, на платных подписках ограничения мягче.
- Лимит касается только Docker Hub. У
ghcr.io,quay.io,public.ecr.awsи реестров облаков свои правила. - Точные цифры Docker уже несколько раз менял. Проверьте актуальное значение в документации Docker, раздел «Usage and limits».
Авторизация, в том числе в CI
Самое простое решение: docker login с токеном доступа вместо пароля. Токен создаётся в настройках аккаунта Docker Hub, ему можно дать права только на чтение.
echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdinФлаг --password-stdin не оставляет токен в истории оболочки и в списке процессов. В CI храните токен в секретах пайплайна, в GitHub Actions для этого есть шаг docker/login-action. Если раннеры создаются заново для каждой задачи, логин нужен в начале каждой задачи, иначе запросы снова станут анонимными.
Указывайте в CI конкретный тег или дайджест (image@sha256:...) вместо latest. Сборки станут повторяемыми, а закэшированный на раннере образ не придётся перекачивать из-за того, что кто-то обновил плавающий тег.
В Kubernetes образы тянут сами узлы, поэтому учётные данные передают через imagePullSecrets. Заодно проверьте imagePullPolicy: с Always каждый запуск пода обращается к реестру, а IfNotPresent использует образ, который уже лежит на узле.
Зеркала и кэширующие прокси
Для популярных образов с Docker Hub можно указать зеркало в /etc/docker/daemon.json:
{
"registry-mirrors": ["https://mirror.gcr.io"]
}После изменения перезапустите демон: sudo systemctl restart docker. В выводе docker info должен появиться раздел Registry Mirrors. Зеркало работает только для образов Docker Hub. Если нужного образа в нём нет, демон обратится к Docker Hub напрямую.
- Docker Desktop на macOS и Windows: тот же JSON редактируется в Settings, раздел Docker Engine, затем Apply & restart.
- Docker в rootless-режиме на Linux читает
~/.config/docker/daemon.json. - Для команды надёжнее свой кэширующий прокси: proxy cache в Harbor, Dependency Proxy в GitLab, pull through cache в AWS ECR, удалённый репозиторий в Google Artifact Registry. Образ скачивается из Docker Hub один раз, дальше все берут его из кэша.
- Официальные образы Docker также публикуются в ECR Public по пути
public.ecr.aws/docker/library/, напримерpublic.ecr.aws/docker/library/python:3.12-slim. Для других проектов проверьте, нет ли образа вghcr.ioилиquay.io.
Ошибки сети при загрузке
| Сообщение | Что значит |
|---|---|
| net/http: TLS handshake timeout | Соединение с реестром установилось, но шифрованный канал не успел подняться. Перегруженный или нестабильный канал |
| Client.Timeout exceeded while awaiting headers | Реестр не ответил вовремя. Проблема в сети или в прокси |
| unexpected EOF | Слой оборвался посреди загрузки |
| dial tcp: lookup registry-1.docker.io ... no such host | Не работает DNS на машине, где запущен демон |
| i/o timeout | Пакеты уходят, но ответы не возвращаются. Часто межсетевой экран или маршрут |
Важная деталь: образы скачивает не команда docker, а демон dockerd. Переменная HTTPS_PROXY в вашем терминале на него не действует. Прокси для демона задают в daemon.json через ключ proxies или в drop-in файле systemd для службы docker.
Docker Desktop на macOS и Windows запускает демон внутри виртуальной машины. Прокси для него задаётся в Settings, раздел Resources, затем Proxies. Если в системе включён корпоративный прокси, а в Docker Desktop нет, браузер будет работать, а docker pull упадёт по таймауту.
На Linux логи демона удобно смотреть прямо во время загрузки: journalctl -u docker.service -f. Для подробностей включите "debug": true в daemon.json.
Обрывы на больших слоях
Если unexpected EOF и таймауты появляются на тяжёлых слоях, уменьшите число параллельных загрузок и увеличьте число повторов в daemon.json:
{
"max-concurrent-downloads": 2,
"max-download-attempts": 10
}По умолчанию демон качает три слоя одновременно и делает до пяти попыток на каждый. На слабом канале меньше параллельных потоков часто дают более стабильный результат. Чтобы понять, какой слой самый тяжёлый, посмотрите docker history <образ>. Если вы поднимаете сразу много сервисов, docker compose pull качает их образы параллельно, и на слабом канале надёжнее тянуть по одному: docker compose pull web.
Если TLS handshake timeout стабильно повторяется в одной сети и пропадает в другой, проверьте MTU. На Linux ping -M do -s 1472 8.8.8.8, на macOS ping -D -s 1472 8.8.8.8 отправляют пакеты полного размера с запретом фрагментации. Если они не проходят, а с меньшим -s проходят, крупные пакеты по пути теряются, и рукопожатие TLS зависает.
Кэш слоёв при сборке
- Копируйте файлы зависимостей отдельно от исходников: сначала
COPY package*.json ./и установка, потомCOPY . .. Тогда правка кода не сбрасывает слой с зависимостями. - Используйте кэш BuildKit:
RUN --mount=type=cache,target=/root/.npm npm ci. - Берите облегчённые базовые образы (
-slim,-alpine), если проект с ними совместим. - Добавьте
.dockerignoreсnode_modules,.gitи артефактами сборки: контекст меньше, кэш стабильнее. - Многоэтапная сборка (
FROM ... AS build) оставляет в итоговом образе только нужное, и серверам меньше качать. - В CI сохраняйте кэш между запусками:
docker buildx build --cache-from type=registry,ref=<образ>:cache --cache-to type=registry,ref=<образ>:cache,mode=max . - Осторожнее с очисткой:
docker builder pruneудаляет кэш сборки, и следующая сборка снова скачает базовые образы и все зависимости.
Если упирается в маршрут
Если лимит не исчерпан, зеркало настроено, а слои всё равно обрываются, проблема в маршруте до реестра. Стабильное защищённое подключение через ближайшую европейскую локацию обычно убирает обрывы на тяжёлых слоях.
Коротко
toomanyrequestsозначает лимит Docker Hub: выполнитеdocker loginс токеном, в CI тоже.- Остаток лимита проверяйте запросом
HEAD, он не тратит скачивания. - Подключите
mirror.gcr.ioили свой кэширующий прокси. - Прокси для
dockerdзадаётся вdaemon.jsonили systemd, а не в терминале. - На нестабильном канале снизьте
max-concurrent-downloadsи поднимитеmax-download-attempts. - Ускоряйте сборку кэшем BuildKit,
.dockerignoreи многоэтапными Dockerfile.