πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Configuration intermediate ⏱ 20 minutes K8s 1.28+

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.

By Luca Berton β€’ β€’ πŸ“– 7 min read

πŸ’‘ Quick Answer: Create an ImageTagMirrorSet with imageTagMirrors entries mapping each external source registry to your private registry. Set mirrorSourcePolicy: NeverContactSource for air-gapped clusters or AllowContactingSource for 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 out registries.conf changes 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: NeverContactSource

Apply 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.conf

Expected 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.yaml

Granular 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: NeverContactSource

With 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: AllowContactingSource
graph 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:#3b82f6

Common 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 first

ITMS 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 conflict

MCO 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

  1. Mirror images before applying ITMS β€” the redirect happens immediately but images must exist in the mirror
  2. Use NeverContactSource for air-gapped β€” prevents accidental external pulls
  3. Use AllowContactingSource for connected β€” local cache with fallback
  4. More specific sources first β€” docker.io/library before docker.io
  5. Separate ITMS per concern β€” one for NVIDIA, one for Docker Hub, etc. (easier to manage)
  6. Pause GPU MCP before applying β€” prevent GPU node restarts during business hours
  7. Automate mirroring β€” CronJob or oc-mirror pipeline to keep mirrors in sync
  8. 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 --all or oc-mirror, then apply ITMS
#openshift #itms #imagetagmirrorset #registry #mirror #disconnected #airgap
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens