OpenShift Airgapped: Declarative Mirroring with oc-mirror v2
Author: Fabrice JAMMES (LinkedIn). Duration: 25-35 minutes
Objective
In the previous lab you mirrored a single image with skopeo copy and hand-wrote an ImageTagMirrorSet to redirect docker.io pulls to it. That works, but it doesn’t scale: every image needs its own skopeo copy, and the mirror-set YAML must be kept perfectly in sync with whatever you copied — get the library/ namespace wrong and pulls 404.
oc mirror (the v2 plugin, GA since OpenShift 4.16) replaces both steps:
- A single declarative
ImageSetConfigurationlists everything you need (individual images, operator catalogs, even whole OCP release payloads). - One
oc mirror ... --v2invocation mirrors all of it in one pass. - oc-mirror then generates the
ImageTagMirrorSet/ImageDigestMirrorSetmanifests for you, scoped to exactly what it mirrored — no manual YAML.
In this lab you’ll mirror two images (nginx + alpine) in a single pass, apply the mirror set oc-mirror generates, and deploy the same nginx chart — this time with an Alpine sidecar — to confirm both images are transparently redirected.
Prerequisites
- A local clone of
openshift-advanced, withopenshift-advanced/labsas your working directory (cd openshift-advanced/labs) — every relative path below is relative to it - An OpenShift cluster with cluster-admin rights (
oc/kubectlconfigured) — applying mirror sets requires patchingimage.config.openshift.io/cluster(see the previous lab for why this is a node-level, disruptive operation) helmv3+,envsubst, and a container engine able to run a local registry (e.g.podman run registry:2)- The
oc-mirrorv2 plugin matching yourocclient version. Downloadoc-mirror.tar.gzfromhttps://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/stable/, extract it, and place theoc-mirrorbinary in yourPATH(e.g./usr/local/bin). Check it’s wired up:
Note: as of OCP 4.21,
--v2is mandatory —oc-mirror v1still exists behind--v1but is deprecated. Every command below passes--v2explicitly.
- The
nginx-chartused in the Helm on OpenShift migration lab, and itsmirror=trueoption which adds an Alpine sidecar container — used here to prove that both images mirrored in a singleoc mirrorpass get redirected
Pre-requisite — local mirror registry and insecure-registry patch
WARNING — do not start the local registry, this has been performed as a pre-requisite
Same shared
local-registryinstance as the previous lab — check it’s up before continuing:
Do not run the commands below
Step 1 — describe everything to mirror in one ImageSetConfiguration
Render the placeholders and write it to /tmp:
Question: Both images are referenced as docker.io/library/nginx and docker.io/library/alpine — i.e. with the library/ namespace spelled out. Why does that matter, given what you learned in the previous lab?
Step 2 — mirror everything in one pass
This is the mirrorToMirror workflow: oc-mirror reads docker.io/library/{nginx,alpine}, pushes them straight to $LOCAL_REGISTRY, and uses $MIRROR_WORKSPACE/working-dir to keep its state and generated manifests. Output looks like:
Question: The previous lab used skopeo copy ... --dest-tls-verify=false with one invocation per image. What two things does oc mirror --v2 do differently here, just from the flags and output above?
Step 3 — inspect the ImageTagMirrorSet oc-mirror generated for you
(your timestamps/version annotation will differ)
Question: Compare this generated ImageTagMirrorSet with the hand-written one from the previous lab (source: docker.io, mirrors: [$LOCAL_REGISTRY], mirrorSourcePolicy: NeverContactSource). What two differences stand out, and what’s the practical consequence of each?
Step 4 — apply the generated mirror set
As in the previous lab, polling registries.conf directly is more reliable than oc wait machineconfigpool/worker --for=condition=Updated: applying a new ImageTagMirrorSet kicks off a fresh Machine Config Operator rollout, and Updated can briefly still read True from a previous rollout before the new one starts.
Step 5 — deploy nginx + its Alpine sidecar, unchanged
mirror=true adds a second container to the Pod — an alpine:3.19 sidecar running sleep infinity. Like the nginx container, it has no image.registry set: its image reference is the implicit docker.io/library/alpine:3.19, which is exactly the path the ImageTagMirrorSet from Step 4 redirects to $LOCAL_REGISTRY/library.
Step 6 — verify both images came from the mirror
Question: Neither Image nor the Events mention your local registry — same as Approach A in the previous lab. How do the local-registry logs prove both images were actually pulled from the mirror, not from docker.io?
Bonus — rerun oc-mirror: the local cache
Run the exact same command from Step 2 again:
Question: The output still says images to copy 2 and Success copying for both — so nothing was skipped. Yet the second run finishes in roughly a third of the time of the first. Where did the time go the first time, and where is state kept?
Comparing to the previous lab
| Skopeo + hand-written mirror set (previous lab) | oc mirror v2 (this lab) |
|
|---|---|---|
| Mirroring command | One skopeo copy per image |
One oc mirror ... --v2, any number of images/catalogs |
| Mirror set manifest | Hand-written; must match what you copied | Generated from what was actually mirrored |
| Redirect scope | Whatever source:/mirrors: you typed (docker.io, broad) |
Scoped to exactly the namespaces mirrored (docker.io/library) |
mirrorSourcePolicy |
Explicit choice (e.g. NeverContactSource) |
Not set by oc-mirror — defaults to allowing fallback to source |
| Re-running | Re-copies everything every time | Local layer cache (~/.oc-mirror/.cache) skips unchanged downloads |
| Operator catalogs / OCP releases | Not supported by this approach | Same ImageSetConfiguration syntax, just add operators:/platform: |
Cleanup
Key Takeaways
oc mirror --v2is declarative and batched: oneImageSetConfigurationdescribes everything to mirror, one command mirrors it all — no per-imageskopeo copy.--v2is mandatory on currentoc-mirrorclients, and--dest-tls-verify=false(a standard boolean flag) replaces the old v1-only--dest-skip-tls.- oc-mirror generates the
ImageTagMirrorSet/ImageDigestMirrorSetfor you, scoped to exactly the paths it mirrored — but it does not setmirrorSourcePolicy, so review the generated file (and addNeverContactSourceyourself) if you need a hard guarantee against contacting the original source. - A local layer cache (
~/.oc-mirror/.cache) makes iteration cheap — adding images to yourImageSetConfigurationor retrying a failed run doesn’t re-download what’s already cached. - The same
ImageSetConfigurationmechanism is what scales this approach to operator catalogs and full OCP release payloads — the building block for a real disconnected-cluster mirror, not just a couple of test images.
Reference / full solution
- Full demo script:
ex3-airgapped-oc-mirror.sh - ImageSetConfiguration:
imageset-config.yaml - Helm chart:
nginx-chart(seemirror/sidecarvalues) and OpenShift-compatible valuesnginx-values-v2.yaml - Previous lab: OpenShift Airgapped: Mirroring Container Images