Pulls are anonymous and share one address
CommonCI runners, Kubernetes nodes or a whole office pull without logging in, and all of them leave through the same public IP, so they share one anonymous allowance.
Confirm
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-remaining: 0, or close to it, for your address. HEAD does not count as a pull.
Fix
- Authenticate every pull with a Docker Hub account:
docker login -u USERwith a personal access token on build machines. - In GitHub Actions, add docker/login-action with the token from a secret before any step that pulls.
- In Kubernetes, create a docker-registry Secret for docker.io and reference it from imagePullSecrets (or from the namespace's service account), so the kubelet pulls as you.
kubectl create secret docker-registry dockerhub --docker-server=https://index.docker.io/v1/ --docker-username=USER --docker-password=TOKEN -n NAMESPACE
Then add imagePullSecrets: [{name: dockerhub}] to the pod spec or the service account.