ITMS External-to-External Registry Mirroring
Configure OpenShift ImageTagMirrorSet to map external registries to your private registry. Mirror Docker Hub, GHCR, Quay.io, and NVIDIA NGC.
π‘ Quick Answer: Create an
ImageTagMirrorSetwithimageTagMirrorsentries mapping each external source registry to your private registry. SetmirrorSourcePolicy: NeverContactSourcefor air-gapped clusters orAllowContactingSourcefor fallback. Each entry maps a source prefix (e.g.,docker.io/library) to one or more mirrors (e.g.,registry.example.com/dockerhub). Apply the ITMS β MCO rolls outregistries.confchanges to all nodes.
The Problem
Production OpenShift clusters pull images from multiple external registries β Docker Hub, GHCR, Quay.io, NVIDIA NGC, Google GCR, and AWS ECR. This creates problems:
- Rate limits β Docker Hub limits anonymous pulls to 100/6h, authenticated to 200/6h
- Air-gapped clusters β disconnected environments canβt reach external registries
- Compliance β regulated industries require all images sourced from approved internal registries
- Reliability β external registry outages break deployments
- Egress costs β cloud clusters pay for outbound traffic to external registries
The Solution
Complete External-to-External Registry Map
# itms-all-registries.yaml
apiVersion: config.openshift.io/v1
kind: ImageTagMirrorSet
metadata:
name: external-registry-mirrors
spec:
imageTagMirrors:
# ============================================
# Docker Hub β Private Registry
# ============================================
# Official images (docker.io/library/*)
- source: docker.io/library
mirrors:
- registry.example.com/dockerhub/library
mirrorSourcePolicy: NeverContactSource
# Docker Hub user/org images (docker.io/*)
- source: docker.io
mirrors:
- registry.example.com/dockerhub
mirrorSourcePolicy: NeverContactSource
# ============================================
# GitHub Container Registry β Private Registry
# ============================================
- source: ghcr.io
mirrors:
- registry.example.com/ghcr
mirrorSourcePolicy: NeverContactSource
# ============================================
# Quay.io β Private Registry
# ============================================
- source: quay.io
mirrors:
- registry.example.com/quay
mirrorSourcePolicy: NeverContactSource
# ============================================
# Red Hat Registries β Private Registry
# ============================================
- source: registry.redhat.io
mirrors:
- registry.example.com/redhat
mirrorSourcePolicy: NeverContactSource
- source: registry.access.redhat.com
mirrors:
- registry.example.com/redhat-access
mirrorSourcePolicy: NeverContactSource
# ============================================
# NVIDIA NGC β Private Registry
# ============================================
- source: nvcr.io
mirrors:
- registry.example.com/nvidia
mirrorSourcePolicy: NeverContactSource
# ============================================
# Google Container Registry β Private Registry
# ============================================
- source: gcr.io
mirrors:
- registry.example.com/gcr
mirrorSourcePolicy: NeverContactSource
- source: us-docker.pkg.dev
mirrors:
- registry.example.com/google-gar
mirrorSourcePolicy: NeverContactSource
# ============================================
# AWS ECR Public β Private Registry
# ============================================
- source: public.ecr.aws
mirrors:
- registry.example.com/ecr-public
mirrorSourcePolicy: NeverContactSource
# ============================================
# Kubernetes Registry β Private Registry
# ============================================
- source: registry.k8s.io
mirrors:
- registry.example.com/k8s
mirrorSourcePolicy: NeverContactSource
# ============================================
# Microsoft MCR β Private Registry
# ============================================
- source: mcr.microsoft.com
mirrors:
- registry.example.com/mcr
mirrorSourcePolicy: NeverContactSource
# ============================================
# Elastic β Private Registry
# ============================================
- source: docker.elastic.co
mirrors:
- registry.example.com/elastic
mirrorSourcePolicy: NeverContactSource
# ============================================
# GitLab Registry β Private Registry
# ============================================
- source: registry.gitlab.com
mirrors:
- registry.example.com/gitlab
mirrorSourcePolicy: NeverContactSourceApply and Verify
# Apply the ITMS
oc apply -f itms-all-registries.yaml
# Watch MCO rollout (triggers node-by-node restart)
oc get mcp -w
# NAME CONFIG UPDATED UPDATING DEGRADED MACHINECOUNT READYMACHINECOUNT
# master ... True False False 3 3
# worker ... False True False 5 3
# Wait for all nodes to be updated
oc wait mcp/worker --for=condition=Updated --timeout=30m
# Verify registries.conf on a node
oc debug node/worker-01 -- chroot /host cat /etc/containers/registries.conf.d/99-itms-external-registry-mirrors.confExpected registries.conf output:
[[registry]]
prefix = ""
location = "docker.io/library"
mirror-by-digest-only = false
[[registry.mirror]]
location = "registry.example.com/dockerhub/library"
[[registry]]
prefix = ""
location = "docker.io"
mirror-by-digest-only = false
[[registry.mirror]]
location = "registry.example.com/dockerhub"
[[registry]]
prefix = ""
location = "nvcr.io"
mirror-by-digest-only = false
[[registry.mirror]]
location = "registry.example.com/nvidia"
# ... (one block per source)Mirror Images with skopeo
#!/bin/bash
# mirror-images.sh β Sync external images to private registry
DEST="registry.example.com"
CREDS="--dest-creds admin:${REGISTRY_PASSWORD}"
# Docker Hub official images
for img in nginx:1.27 redis:7.4 postgres:16 python:3.12-slim node:22-slim; do
echo "Mirroring docker.io/library/${img}..."
skopeo copy --all \
docker://docker.io/library/${img} \
docker://${DEST}/dockerhub/library/${img} ${CREDS}
done
# NVIDIA NGC images
for img in \
"nvidia/tritonserver:24.12-trtllm-python-py3" \
"nvidia/cuda:12.6.3-devel-ubi9" \
"nvidia/nemo:24.12" \
"nvidia/gpu-operator:v24.9.2"; do
echo "Mirroring nvcr.io/${img}..."
skopeo copy --all \
docker://nvcr.io/${img} \
docker://${DEST}/nvidia/${img} ${CREDS}
done
# Quay.io images
for img in \
"argoproj/argocd:v2.13.3" \
"coreos/etcd:v3.5.17" \
"strimzi/operator:0.44.0"; do
echo "Mirroring quay.io/${img}..."
skopeo copy --all \
docker://quay.io/${img} \
docker://${DEST}/quay/${img} ${CREDS}
done
# GitHub Container Registry
for img in \
"vllm-project/vllm-openai:v0.6.6" \
"cert-manager/cert-manager-controller:v1.16.3"; do
echo "Mirroring ghcr.io/${img}..."
skopeo copy --all \
docker://ghcr.io/${img} \
docker://${DEST}/ghcr/${img} ${CREDS}
done
echo "Mirror sync complete"oc-mirror for Bulk Mirroring
# imageset-config.yaml
apiVersion: mirror.openshift.io/v1alpha2
kind: ImageSetConfiguration
mirror:
additionalImages:
# Docker Hub
- name: docker.io/library/nginx:1.27
- name: docker.io/library/redis:7.4
- name: docker.io/library/postgres:16
# NVIDIA
- name: nvcr.io/nvidia/tritonserver:24.12-trtllm-python-py3
- name: nvcr.io/nvidia/cuda:12.6.3-devel-ubi9
# Quay.io
- name: quay.io/argoproj/argocd:v2.13.3
# GHCR
- name: ghcr.io/vllm-project/vllm-openai:v0.6.6# Run oc-mirror to sync all images and generate ITMS
oc-mirror --config imageset-config.yaml \
docker://registry.example.com/mirror \
--dest-skip-tls
# oc-mirror auto-generates ITMS/IDMS manifests
ls oc-mirror-workspace/results-*/
# imageContentSourcePolicy.yaml (legacy)
# imageTagMirrorSet.yaml (OCP 4.13+)
# Apply the generated ITMS
oc apply -f oc-mirror-workspace/results-*/imageTagMirrorSet.yamlGranular Per-Namespace Mapping
# For specific org/project mappings within a registry
apiVersion: config.openshift.io/v1
kind: ImageTagMirrorSet
metadata:
name: project-specific-mirrors
spec:
imageTagMirrors:
# Map specific Docker Hub orgs to separate paths
- source: docker.io/bitnami
mirrors:
- registry.example.com/dockerhub/bitnami
mirrorSourcePolicy: NeverContactSource
- source: docker.io/grafana
mirrors:
- registry.example.com/dockerhub/grafana
mirrorSourcePolicy: NeverContactSource
# Map NVIDIA sub-paths separately
- source: nvcr.io/nvidia/tritonserver
mirrors:
- registry.example.com/nvidia/triton
mirrorSourcePolicy: NeverContactSource
- source: nvcr.io/nvidia/nemo
mirrors:
- registry.example.com/nvidia/nemo
mirrorSourcePolicy: NeverContactSource
- source: nvcr.io/nvidia/cuda
mirrors:
- registry.example.com/nvidia/cuda
mirrorSourcePolicy: NeverContactSourceWith Fallback (Connected Clusters)
# itms-with-fallback.yaml
# For connected clusters that want local caching with external fallback
apiVersion: config.openshift.io/v1
kind: ImageTagMirrorSet
metadata:
name: registry-cache-with-fallback
spec:
imageTagMirrors:
# Try private mirror first, fall back to source
- source: docker.io
mirrors:
- registry.example.com/dockerhub
mirrorSourcePolicy: AllowContactingSource # β fallback enabled
- source: nvcr.io
mirrors:
- registry.example.com/nvidia
mirrorSourcePolicy: AllowContactingSource
- source: ghcr.io
mirrors:
- registry.example.com/ghcr
mirrorSourcePolicy: AllowContactingSourcegraph LR
subgraph External Registries
DH[docker.io]
GH[ghcr.io]
QI[quay.io]
NV[nvcr.io]
RH[registry.redhat.io]
GCR[gcr.io]
end
subgraph ITMS Mapping
ITMS[ImageTagMirrorSet]
end
subgraph Private Registry
DHM[/dockerhub/]
GHM[/ghcr/]
QIM[/quay/]
NVM[/nvidia/]
RHM[/redhat/]
GCRM[/gcr/]
end
DH -->|mirrored| DHM
GH -->|mirrored| GHM
QI -->|mirrored| QIM
NV -->|mirrored| NVM
RH -->|mirrored| RHM
GCR -->|mirrored| GCRM
ITMS -.->|redirects pulls| DHM
ITMS -.->|redirects pulls| GHM
ITMS -.->|redirects pulls| NVM
style ITMS fill:#dbeafe,stroke:#3b82f6Common Issues
More Specific Source Must Come First
# β WRONG β docker.io catches everything before docker.io/library
- source: docker.io
mirrors: [registry.example.com/dockerhub]
- source: docker.io/library
mirrors: [registry.example.com/dockerhub/library]
# β
CORRECT β more specific source first
- source: docker.io/library
mirrors: [registry.example.com/dockerhub/library]
- source: docker.io
mirrors: [registry.example.com/dockerhub]Image Not Found After ITMS Applied
# Verify image exists in mirror
skopeo inspect docker://registry.example.com/dockerhub/library/nginx:1.27
# Common cause: image not yet mirrored to private registry
# ITMS redirects pulls but doesn't copy images β you must mirror firstITMS vs IDMS β When to Use Each
# ITMS (ImageTagMirrorSet) β for tag-based references (:latest, :v1.2)
# Use when: workloads reference images by tag
# registries.conf: mirror-by-digest-only = false
# IDMS (ImageDigestMirrorSet) β for digest-based references (@sha256:...)
# Use when: workloads pin images by digest (more secure)
# registries.conf: mirror-by-digest-only = true
# You can use BOTH simultaneously β they don't conflictMCO Stuck After Applying ITMS
# Check MCP status
oc get mcp
# If DEGRADED=True:
oc describe mcp worker | grep -A5 "Degraded"
# Check node status
oc get nodes
oc debug node/<degraded-node> -- chroot /host journalctl -u crio --since "10m ago" | tail -20
# Force re-render if stuck
oc patch mcp/worker --type merge -p '{"spec":{"paused":false}}'Best Practices
- Mirror images before applying ITMS β the redirect happens immediately but images must exist in the mirror
- Use
NeverContactSourcefor air-gapped β prevents accidental external pulls - Use
AllowContactingSourcefor connected β local cache with fallback - More specific sources first β
docker.io/librarybeforedocker.io - Separate ITMS per concern β one for NVIDIA, one for Docker Hub, etc. (easier to manage)
- Pause GPU MCP before applying β prevent GPU node restarts during business hours
- Automate mirroring β CronJob or oc-mirror pipeline to keep mirrors in sync
- Test with
skopeo inspectβ verify mirror accessibility before applying ITMS
Key Takeaways
- ITMS maps source registry prefixes to private registry mirrors transparently
- Pods reference original image names β CRI-O redirects to mirrors via
registries.conf NeverContactSource= air-gapped (hard fail if mirror missing),AllowContactingSource= caching proxy (fallback to source)- Apply ITMS triggers MCO rolling restart β plan for maintenance window
- Mirror images first with
skopeo copy --alloroc-mirror, then apply ITMS

Recommended
Kubernetes Recipes β The Complete Book100+ production-ready patterns with detailed explanations, best practices, and copy-paste YAML. Everything in one place.
Get the Book βLearn by Doing
CopyPasteLearn β Hands-on Cloud & DevOps CoursesMaster Kubernetes, Ansible, Terraform, and MLOps with interactive, copy-paste-run lessons. Start free.
Browse Courses βπ Deepen Your Skills β Hands-on Courses
Courses by CopyPasteLearn.com β Learn IT by Doing
