IranVia
Educational Articles

Docker Hub Mirror in Iran: Bypass Sanctions and Pull Images Fast

9 min
Docker Hub Mirror in Iran: Bypass Sanctions and Pull Images Fast

To get around Docker Hub restrictions in Iran, simply prepend docker.novin.cloud to the image name, or register it as the default mirror in your daemon.json. The Novin Cloud Docker mirror works without registration and without a VPN, is built on the Docker Registry v2 standard, and serves every docker pull from domestic infrastructure, so your builds finish in seconds instead of failing.

Below we cover both configuration methods, containerd and Kubernetes settings, mirrors for other registries such as ghcr and quay, and a practical troubleshooting section. If you have already solved npm install problems in Iran with a mirror, the logic here will feel familiar; only the tooling layer has changed.

Why does docker pull fail in Iran?

When you run docker pull nginx, Docker connects to registry-1.docker.io behind the scenes, obtains an anonymous token, and then downloads the image layers from Docker Hub's content delivery network. From inside Iran, all three steps can fail: access is restricted on the provider side, the network path has high latency, and downloads of hundred-megabyte layers get cut off midway.

Add an official limitation on top of that: Docker Hub caps unauthenticated users at 100 pulls per 6-hour window per IPv4 address (or per /64 subnet on IPv6), and free personal accounts at 200 pulls per 6 hours. In an office network where dozens of developers and CI runners share a single IP, that ceiling fills far sooner than you would expect and you start receiving 429 responses.

Three errors you have probably seen

Error messageRoot causeFix
error pulling image configuration ... 403 ForbiddenGeographic restriction on the registry sideUse a domestic mirror
net/http: TLS handshake timeoutOutage or latency on the international routeUse a domestic mirror
toomanyrequests: You have reached your pull rate limitExceeding the Docker Hub pull ceilingImages cached on the mirror

The important point is that none of these three is solved by restarting Docker or changing DNS. The only durable fix is changing where images come from.

What is a Docker registry mirror and how does it work?

A registry mirror, also called a pull-through cache, is an intermediate registry that sits between your Docker client and the upstream registry. The first time an image is requested, the mirror fetches it from the original source, stores it on its own disk, and hands it to you. On subsequent requests the same image is served straight from local storage.

Know two things from the outset so your expectations stay accurate. First, a mirror is for pulls only; docker push is not supported against it, so publishing your own private images requires a dedicated registry. Second, in the Docker standard the registry-mirrors key only affects images that come from Docker Hub, and does not cover images such as ghcr.io/owner/app. You can read the official explanation of this behavior in the Docker documentation. For those registries we use separate mirror paths, covered further below.

The Novin Cloud Docker registry plays exactly this role: it is public, requires no login, and runs on domestic infrastructure. If your server or cluster runs on that same infrastructure, the image path stays almost entirely inside the network.

Method 1: use the mirror domain directly, no config changes

The simplest route, which works even on a personal laptop without root access, is to prepend the mirror name to the image name. You edit no files and restart no services.

docker pull docker.novin.cloud/library/nginx
docker pull docker.novin.cloud/hello-world
docker pull docker.novin.cloud/bitnami/postgresql

The generic mirror path is available too and serves the same content; it is longer but more explicit:

docker pull mirror.novin.cloud/docker/library/nginx

You can use the same approach inside a Dockerfile so the build step stays reproducible in any environment:

FROM docker.novin.cloud/library/node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

If you call the /v2/ path with curl and get a 401 back, do not worry; that is part of the standard token flow in the Docker protocol and the Docker client handles it automatically. More detail is available in the Novin Cloud Docker and OCI documentation.

Method 2: set a global mirror in daemon.json

If you want every docker pull on a server to go through the mirror without renaming images, register the mirror at the Docker daemon level. This is the best option for team servers, build machines, and virtual servers, because your code and manifests stay untouched.

  1. Open /etc/docker/daemon.json (create it if it does not exist).
  2. Add the registry-mirrors key.
  3. Restart the Docker service.
  4. Verify the result with a test pull.
sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
  "registry-mirrors": ["https://docker.novin.cloud"]
}
EOF

sudo systemctl restart docker

If the file already contains other keys such as log-driver, be sure to preserve them; overwriting the whole file is one of the most common mistakes at this step. To confirm the JSON is valid before restarting, use jq.

jq . /etc/docker/daemon.json
docker info | grep -A2 "Registry Mirrors"
docker pull hello-world

The docker info output should show the mirror address. From then on even a plain docker pull redis:7 is served by the domestic mirror without any prefix.

Rolling back

To roll back, remove the registry-mirrors key from the file and restart the service. Direct pulls using the domain prefix need no rollback at all; just use the full docker.io/... name again.

Configuring containerd and Kubernetes nodes

Modern Kubernetes clusters use containerd rather than Docker, so setting daemon.json on those nodes has no effect. containerd offers two approaches. The older one lives in /etc/containerd/config.toml:

[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
  endpoint = ["https://docker.novin.cloud"]

The newer, recommended approach is a hosts.toml file at /etc/containerd/certs.d/docker.io/hosts.toml, which is cleaner to manage with configuration tooling:

server = "https://docker.io"

[host."https://docker.novin.cloud"]
  capabilities = ["pull", "resolve"]
sudo systemctl restart containerd
sudo ctr images pull docker.io/library/alpine:3.20

On a cluster, distribute that same file with Ansible or bake it into the node base image so every pod pulls through the mirror with no manifest changes. If you bootstrap the cluster with kubeadm, system images such as pause and etcd should come from the mirror as well:

kubeadm init --image-repository mirror.novin.cloud/docker-k8s
[plugins."io.containerd.grpc.v1.cri"]
  sandbox_image = "mirror.novin.cloud/docker-k8s/pause:3.10"

We walk through bringing a cluster up step by step in our quick Kubernetes setup article, and if you would rather not maintain a control plane yourself, managed Kubernetes ships with these settings already applied on the nodes. Full technical detail is available in the containerd documentation and the Kubernetes documentation.

Mirroring other registries: ghcr, quay, gcr and the rest

As noted, the registry-mirrors key only covers Docker Hub. Other registries each have their own mirror repository, and you simply rewrite the image name:

Upstream registryMirror repositoryExample
ghcr.iodocker-ghcrmirror.novin.cloud/docker-ghcr/owner/image
quay.iodocker-quaymirror.novin.cloud/docker-quay/prometheus/busybox
gcr.iodocker-gcrmirror.novin.cloud/docker-gcr/project/image
registry.k8s.iodocker-k8smirror.novin.cloud/docker-k8s/pause:3.10
mcr.microsoft.comdocker-mcrmirror.novin.cloud/docker-mcr/dotnet/sdk
nvcr.iodocker-nvidiamirror.novin.cloud/docker-nvidia/nvidia/cuda
registry.redhat.iodocker-redhatmirror.novin.cloud/docker-redhat/ubi9/ubi
registry.gitlab.comdocker-gitlabmirror.novin.cloud/docker-gitlab/group/project
public.ecr.awsdocker-ecr-publicmirror.novin.cloud/docker-ecr-public/ns/image

Installing Docker Engine itself from the mirror

On a freshly provisioned server, even installing Docker can fail because download.docker.com is unreachable. The Docker apt repository is mirrored as well:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://mirror.novin.cloud/docker-apt-ubuntu/gpg |   sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://mirror.novin.cloud/docker-apt-ubuntu noble stable" |   sudo tee /etc/apt/sources.list.d/docker.list

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin

On CentOS and RHEL, the docker-yum-centos and docker-yum-rhel repositories follow the same layout as the official paths. Static Docker binaries are also available from mirror.novin.cloud/docker-static-binaries/. The full repository list is on the mirrors service page and in the repository index documentation.

Using it in your CI/CD pipeline

The mirror pays off most in the CI pipeline, where every commit triggers a fresh build and a failed pull means a failed build. In GitLab Runner you only change the image prefix, and no authentication or VPN is needed:

build:
  image: docker.novin.cloud/library/node:20
  script:
    - npm ci
    - npm run build

If you manage the runners yourself, it is better to register the mirror once in that machine's daemon.json so every project benefits without touching its config file. For small teams and startups, this single change usually shaves minutes off each build and noticeably lowers the failure rate of nightly builds.

Troubleshooting: when the mirror does not work

If you still see errors after configuring it, check these four things in order.

  1. Invalid JSON: if daemon.json is malformed, the Docker service will not start at all. Inspect the logs with systemctl status docker and journalctl -u docker -n 50.
  2. The image comes from another registry: if the image name starts with ghcr.io or quay.io, the mirror key does not apply. Use the repository table above.
  3. Stale layer cache: pull again and check docker image inspect to confirm the image digest is what you expect.
  4. Local proxy or firewall: if HTTP_PROXY variables are set on the server, mirror traffic may be routed through them too. Check the Docker service environment in /etc/systemd/system/docker.service.d/.

For a final check, compare the image digest. What you receive from the mirror should be exactly what the original source would have delivered:

docker pull docker.novin.cloud/library/hello-world
docker image inspect hello-world --format '{{.RepoDigests}}'

Frequently asked questions

Do I need to register to use the Novin Cloud Docker mirror?

No. The Docker repository is public and docker pull works without logging in. If you call the /v2/ path with curl and see a 401, that response is normal and part of the anonymous token flow in the standard Docker protocol.

Can I push my own image to this mirror?

No. The mirror is a pull-through cache only and push operations are not supported. To store private images you need a dedicated registry, usually running on your own server or cluster.

Does registry-mirrors also affect ghcr.io images?

No. By Docker's own design, the registry-mirrors key applies only to images originating from Docker Hub. For ghcr, quay, gcr and others you must rewrite the image name with the matching mirror repository prefix.

Which method is right for Kubernetes?

If your nodes use containerd, which is the default in recent Kubernetes versions, configure hosts.toml or the registry.mirrors block in containerd; daemon.json has no effect on those nodes. Cluster system images should come from the docker-k8s repository.

Does a mirror really speed up builds?

In practice most of the difference comes from eliminating failures and retries rather than raw bandwidth. Once the pull step becomes predictable, total build time stabilizes too. The exact improvement depends on image size, network path, and where your server sits, so measure the same build before and after the change.

Conclusion

Docker Hub sanctions are not a networking glitch you can fix with DNS or a restart; the right answer is to change where images come from. With one line in daemon.json, or by prefixing image names with docker.novin.cloud, you leave behind 403 and timeout errors and stop running into Docker Hub's 100-pull-per-6-hour ceiling. For Kubernetes nodes, do the same in containerd and source system images from the docker-k8s repository.

Novin Cloud, sometimes written by users as Abr Novin, offers this mirror publicly with no registration required. To get started, visit the Novin Cloud Docker registry page or simply pull your first image from docker.novin.cloud.