Configuring Registry Mirroring#
Note
Local registry mirroring requires manual configuration. Automated registry mirroring with support for both x86 and ARM container images is planned for a future release. This page will be updated as availability is confirmed.
Registry mirroring is required for every air-gapped NMC deployment. The
cluster has no route to the upstream registries, so you must configure the
container runtime on every node to redirect image pulls to your local
registry. Otherwise, image pulls fail and component installations stall.
Complete the configuration on this page before you install any NMC component.
If pods enter ImagePullBackOff during an air-gapped installation, registry
mirroring is almost always the cause.
Mirror the following upstream registries so that NMC can pull images without changing chart image references:
nvcr.iodocker.ioghcr.ioquay.ioreg.kyverno.ioregistry.k8s.io
Configure a mirror for each upstream registry that your deployment uses. The following BCM and RKE2 Examples cover all six registries listed here. Use the example that matches your environment.
A containerd mirror changes the registry host but preserves the repository
path and tag. For example, a pull for nvcr.io/<namespace>/<image>:<tag>
asks the local mirror for <namespace>/<image>:<tag>. The image-import
process must populate that same repository path and tag in the local registry.
If it imports the image at the registry root instead, fix the import mapping;
the mirror configuration cannot remove <namespace>/ from a request.
Validate at least one image with a nested repository path through its original
upstream name before installing a component. A successful pull of an image at
the registry root does not verify that nested paths are available.
BCM Example (Most Deployments)#
Deploy the local container registry before you configure mirroring. The
cm-container-registry-setup wizard, described in
Step 4: Set up the container registry, deploys the registry and generates its
CA certificate at
/cm/local/apps/containerd/var/etc/certs.d/<registry-host>:<port>/ca.crt.
If you have not yet deployed the registry, the steps that copy that
certificate into the node trust store fail with cp: cannot stat.
The following example uses the BCM local registry at master.cm.cluster:5000.
Change the address if your registry runs elsewhere. Run the complete script in
one shell on the BCM head node. It reads cmsh JSON with jq to find the
containerd overlays and their assigned categories. For overlays assigned
directly to nodes, cmsh get nodes expands node ranges before the script
looks up each node’s category. It then reads each category’s software image
from cmsh.
On the BCM head node, confirm that containerd imports the additional configuration directory:
cat /cm/local/apps/containerd/var/etc/config.toml
The output is similar to the following. The version value depends on the
containerd release. The line that matters is imports, which points to the
directory that holds all other containerd configuration:
version = 4
imports = ["/cm/local/apps/containerd/var/etc/conf.d/*.toml"]
Add the Mirror Configuration#
The script checks that the registry CA and every category software image exist.
It creates a hosts.toml file for each upstream registry on the BCM head node.
It adds missing mirror entries to existing containerd overlays, updates
existing entries, and prepares every category software image. It updates nodes in populated categories, but
restarts containerd and checks image pulls only for categories with a
containerd overlay. This prepares categories such as k8s-user and
dgx-gb300-k8s before their clusters and overlays are installed.
Setting the local registry as server prevents fallback to an upstream registry when a local image is missing.
BCM can regenerate containerd configuration during imageupdate, so the
script updates existing overlays before updating images.
The final loop waits for containerd and checks an image pull on each populated
category with an existing containerd overlay. Import
registry.k8s.io/pause:3.10.1 into the local registry before running the
script, or change TEST_IMAGE to an image that you have imported. BCM can
create the registry directory in a software image as an absolute symbolic
link. The script writes the CA to the link’s target inside the image.
If both k8s-admin and k8s-user are already deployed, run the script
once to configure both clusters. Rerun it after you deploy any additional
Kubernetes cluster, and before you install components on that cluster.
On the BCM head node, create a script that you can run again after each cluster
deployment. Change REGISTRY_HOST, REGISTRY_PORT, or TEST_IMAGE in
the file if your deployment uses different values.
Registry Mirroring Script
cat > /root/configure-registry-mirroring.sh <<'BASH'
#!/usr/bin/env bash
set -euo pipefail
REGISTRY_HOST="master.cm.cluster"
REGISTRY_PORT="5000"
CERTS_D="/cm/local/apps/containerd/var/etc/certs.d"
REGISTRY_CA="${CERTS_D}/${REGISTRY_HOST}:${REGISTRY_PORT}/ca.crt"
TEST_IMAGE="registry.k8s.io/pause:3.10.1"
UPSTREAM_REGISTRIES=(
nvcr.io
docker.io
ghcr.io
quay.io
reg.kyverno.io
registry.k8s.io
)
OVERLAY_JSON=$(cmsh --color=no -c \
"configurationoverlay; list -f name,categories,roles -d {}")
DISCOVERY=$(jq -nr --argjson overlays "${OVERLAY_JSON}" '
def names:
(if type == "array" then .[] else (. // "" | split(",")[]) end)
| gsub("^[[:space:]]+|[[:space:]]+$"; "")
| select(length > 0);
[$overlays[] | select((.roles | names) == "generic::containerd")]
as $selected
| ([$selected[] | .["name (key)"] // .name] | unique[] | "overlay\t\(.)"),
([$selected[] | .categories | names] | unique[] | "category\t\(.)")
')
CONTAINERD_OVERLAYS=()
NODE_CATEGORIES=()
while IFS=$'\t' read -r KIND NAME; do
case "${KIND}" in
overlay) CONTAINERD_OVERLAYS+=("${NAME}") ;;
category) NODE_CATEGORIES+=("${NAME}") ;;
esac
done <<< "${DISCOVERY}"
for OVERLAY_NAME in "${CONTAINERD_OVERLAYS[@]}"; do
OVERLAY_NODES=$(cmsh --color=no -c \
"configurationoverlay; use ${OVERLAY_NAME}; get nodes")
while IFS= read -r NODE_NAME; do
[[ -n "${NODE_NAME}" ]] || continue
NODE_CATEGORY=$(cmsh --color=no -c \
"device; use ${NODE_NAME}; get category")
[[ -n "${NODE_CATEGORY}" ]] && NODE_CATEGORIES+=("${NODE_CATEGORY}")
done <<< "${OVERLAY_NODES}"
done
declare -A CATEGORY_SET=()
for NODE_CATEGORY in "${NODE_CATEGORIES[@]}"; do
CATEGORY_SET["${NODE_CATEGORY}"]=1
done
NODE_CATEGORIES=("${!CATEGORY_SET[@]}")
CATEGORY_ROWS=$(cmsh --color=no -c \
"category; list -f name,softwareimage,nodes -d {}" | jq -r '
.[] | select((.softwareimage // "") != "")
| [(."name (key)" // .name), .softwareimage, (.nodes | tonumber? // 0)]
| @tsv
')
ALL_CATEGORIES=()
CATEGORY_IMAGES=()
CATEGORY_NODE_COUNTS=()
while IFS=$'\t' read -r CATEGORY_NAME IMAGE_NAME NODE_COUNT; do
[[ -n "${CATEGORY_NAME}" ]] || continue
ALL_CATEGORIES+=("${CATEGORY_NAME}")
CATEGORY_IMAGES+=("${IMAGE_NAME}")
CATEGORY_NODE_COUNTS+=("${NODE_COUNT}")
done <<< "${CATEGORY_ROWS}"
if ((${#ALL_CATEGORIES[@]} == 0)); then
echo "No category software images found in cmsh" >&2
exit 1
fi
test -f "${REGISTRY_CA}" || { echo "Missing ${REGISTRY_CA}" >&2; exit 1; }
for INDEX in "${!ALL_CATEGORIES[@]}"; do
test -d "/cm/images/${CATEGORY_IMAGES[INDEX]}" || {
echo "Missing software image for ${ALL_CATEGORIES[INDEX]}: ${CATEGORY_IMAGES[INDEX]}" >&2
exit 1
}
printf 'Category %s: %s (%s nodes)\n' \
"${ALL_CATEGORIES[INDEX]}" "${CATEGORY_IMAGES[INDEX]}" \
"${CATEGORY_NODE_COUNTS[INDEX]}"
done
printf 'Existing containerd overlays: %s\n' "${CONTAINERD_OVERLAYS[*]:-none}"
for UPSTREAM_REGISTRY in "${UPSTREAM_REGISTRIES[@]}"; do
mkdir -p "${CERTS_D}/${UPSTREAM_REGISTRY}"
tee "${CERTS_D}/${UPSTREAM_REGISTRY}/hosts.toml" >/dev/null << EOF
server = "https://${REGISTRY_HOST}:${REGISTRY_PORT}"
[host."https://${REGISTRY_HOST}:${REGISTRY_PORT}"]
capabilities = ["pull", "resolve"]
ca = "${REGISTRY_CA}"
EOF
done
mkdir -p /usr/local/share/ca-certificates
cp "${REGISTRY_CA}" "/usr/local/share/ca-certificates/${REGISTRY_HOST}-registry.crt"
update-ca-certificates
for OVERLAY_NAME in "${CONTAINERD_OVERLAYS[@]}"; do
ENV_JSON=$(cmsh --color=no -c "configurationoverlay; use ${OVERLAY_NAME}; roles; use generic::containerd; environments; list -f name -d {}")
if jq -e --arg name registry_mirror 'any(.[]; (."name (key)" // .name // "") == $name)' <<< "${ENV_JSON}" >/dev/null; then
MIRROR_ACTION=use
else
MIRROR_ACTION=add
fi
if jq -e --arg name registry_mirror_ca_entry 'any(.[]; (."name (key)" // .name // "") == $name)' <<< "${ENV_JSON}" >/dev/null; then
CA_ACTION=use
else
CA_ACTION=add
fi
cmsh --color=no -c "configurationoverlay; use ${OVERLAY_NAME}; roles; use generic::containerd; environments; ${MIRROR_ACTION} registry_mirror; set value https://${REGISTRY_HOST}:${REGISTRY_PORT}; commit"
cmsh --color=no -c "configurationoverlay; use ${OVERLAY_NAME}; roles; use generic::containerd; environments; ${CA_ACTION} registry_mirror_ca_entry; set value \"ca = '${REGISTRY_CA}'\"; commit"
ACTUAL_MIRROR=$(cmsh --color=no -c "configurationoverlay; use ${OVERLAY_NAME}; roles; use generic::containerd; environments; use registry_mirror; get value")
ACTUAL_CA=$(cmsh --color=no -c "configurationoverlay; use ${OVERLAY_NAME}; roles; use generic::containerd; environments; use registry_mirror_ca_entry; get value")
if [[ "${ACTUAL_MIRROR}" != "https://${REGISTRY_HOST}:${REGISTRY_PORT}" ||
"${ACTUAL_CA}" != "ca = '${REGISTRY_CA}'" ]]; then
echo "Containerd mirror values were not saved in ${OVERLAY_NAME}" >&2
exit 1
fi
done
declare -A PREPARED_IMAGES=()
declare -A NODE_COUNT_BY_CATEGORY=()
for INDEX in "${!ALL_CATEGORIES[@]}"; do
NODE_COUNT_BY_CATEGORY["${ALL_CATEGORIES[INDEX]}"]="${CATEGORY_NODE_COUNTS[INDEX]}"
done
for INDEX in "${!ALL_CATEGORIES[@]}"; do
NODE_CATEGORY="${ALL_CATEGORIES[INDEX]}"
NODE_IMAGE="${CATEGORY_IMAGES[INDEX]}"
IMAGE_ROOT="/cm/images/${NODE_IMAGE}"
if [[ -z "${PREPARED_IMAGES[$NODE_IMAGE]:-}" ]]; then
mkdir -p "${IMAGE_ROOT}/usr/local/share/ca-certificates"
cp -fv "${REGISTRY_CA}" \
"${IMAGE_ROOT}/usr/local/share/ca-certificates/${REGISTRY_HOST}-registry.crt"
IMAGE_REGISTRY_DIR="${IMAGE_ROOT}${CERTS_D}/${REGISTRY_HOST}:${REGISTRY_PORT}"
if [[ -L "${IMAGE_REGISTRY_DIR}" ]]; then
LINK_TARGET=$(readlink "${IMAGE_REGISTRY_DIR}")
if [[ "${LINK_TARGET}" == /* ]]; then
IMAGE_CA="${IMAGE_ROOT}${LINK_TARGET}/ca.crt"
else
IMAGE_CA="$(realpath -m "$(dirname "${IMAGE_REGISTRY_DIR}")/${LINK_TARGET}")/ca.crt"
fi
else
IMAGE_CA="${IMAGE_REGISTRY_DIR}/ca.crt"
fi
mkdir -p "$(dirname "${IMAGE_CA}")"
cp -fv "${REGISTRY_CA}" "${IMAGE_CA}"
for UPSTREAM_REGISTRY in "${UPSTREAM_REGISTRIES[@]}"; do
mkdir -p "${IMAGE_ROOT}${CERTS_D}/${UPSTREAM_REGISTRY}"
cp -fv "${CERTS_D}/${UPSTREAM_REGISTRY}/hosts.toml" \
"${IMAGE_ROOT}${CERTS_D}/${UPSTREAM_REGISTRY}/"
done
cm-chroot-sw-img "${IMAGE_ROOT}" << EOF
update-ca-certificates
exit
EOF
PREPARED_IMAGES["${NODE_IMAGE}"]=1
fi
if (( CATEGORY_NODE_COUNTS[INDEX] > 0 )); then
cmsh --color=no -c "device; imageupdate -w -c ${NODE_CATEGORY} --wait"
pdsh -g "category=${NODE_CATEGORY}" 'update-ca-certificates'
if [[ -n "${CATEGORY_SET[$NODE_CATEGORY]:-}" ]]; then
pdsh -g "category=${NODE_CATEGORY}" 'systemctl restart containerd'
fi
fi
done
# Apply the head-node changes after all node images are updated.
systemctl restart containerd
for NODE_CATEGORY in "${NODE_CATEGORIES[@]}"; do
[[ "${NODE_COUNT_BY_CATEGORY[$NODE_CATEGORY]:-0}" != 0 ]] || continue
printf '\nTest image pull on category: %s\n' "${NODE_CATEGORY}"
pdsh -g "category=${NODE_CATEGORY}" \
'attempt=0; until crictl --runtime-endpoint unix:///run/containerd/containerd.sock info >/dev/null 2>&1; do attempt=$((attempt + 1)); [ "$attempt" -lt 30 ] || exit 1; sleep 2; done'
pdsh -g "category=${NODE_CATEGORY}" \
"crictl --runtime-endpoint unix:///run/containerd/containerd.sock pull ${TEST_IMAGE}" | dshbak -c
done
BASH
Run the script on the BCM head node:
chmod 700 /root/configure-registry-mirroring.sh
/root/configure-registry-mirroring.sh
Note
Required rerun after cluster deployment. You must rerun the complete script after deploying each additional Kubernetes cluster and before installing NMC components on it. The first run prepares its software image, but its containerd overlays do not exist until cluster deployment. The rerun configures those overlays, restarts containerd on the new cluster’s nodes, and validates the image pull.
/root/configure-registry-mirroring.sh
If your deployment pulls images from another upstream registry, add its
host name to UPSTREAM_REGISTRIES and import its images into the local
registry before rerunning the script.
Containerd reads a registry-specific hosts.toml file in preference to
_default/hosts.toml. For any additional registry, inspect existing
registry-specific files on the BCM head node and in the node images before
relying on the default mirror.
RKE2 Example#
On every server and agent node, create /etc/rancher/rke2/registries.yaml
with a configuration such as the following.
RKE2 reads this file directly and does not expand shell variables, so replace the following values throughout the example:
Value in the example |
Replace with |
|---|---|
|
The hostname and port of your local registry. This value appears in
every |
|
The path on each node to the CA certificate for your registry. |
RKE2 Registry Configuration
# /etc/rancher/rke2/registries.yaml
mirrors:
nvcr.io:
endpoint:
- https://airgap-registry.com:5443
docker.io:
endpoint:
- https://airgap-registry.com:5443
ghcr.io:
endpoint:
- https://airgap-registry.com:5443
quay.io:
endpoint:
- https://airgap-registry.com:5443
reg.kyverno.io:
endpoint:
- https://airgap-registry.com:5443
registry.k8s.io:
endpoint:
- https://airgap-registry.com:5443
configs:
airgap-registry.com:5443:
tls:
ca_file: "/etc/ssl/certs/airgap-registry.pem"
After you create the file, restart RKE2 on each node. Run
systemctl restart rke2-server on server nodes and
systemctl restart rke2-agent on agent nodes.
containerd Example#
Use this example on nodes that BCM does not manage. On a BCM-managed node,
/etc/containerd is a symbolic link to
/cm/local/apps/containerd/var/etc and config_path is already set in
conf.d. Use the BCM example on those nodes, and do not modify
config.toml.
Set the following variables to match your environment. The remaining commands reference these variables, so this is the only block you edit:
REGISTRY_HOST="airgap-registry.com" # Hostname of your local registry
REGISTRY_PORT="5443" # Port of your local registry
CERTS_D="/etc/containerd/certs.d"
Configure containerd to use the certs.d directory and add mirror host
entries:
containerd Mirror Configuration
sudo mkdir -p "${CERTS_D}"
sudo cp /etc/containerd/config.toml /etc/containerd/config.toml.bak
# Ensure containerd uses the certs.d config path
# (add or update config_path)
echo "config_path = \"${CERTS_D}\"" | sudo tee -a /etc/containerd/config.toml
sudo systemctl restart containerd
# Mirror for NVIDIA registry
sudo mkdir -p "${CERTS_D}/nvcr.io"
sudo tee "${CERTS_D}/nvcr.io/hosts.toml" >/dev/null << EOF
server = "https://nvcr.io"
[host."https://${REGISTRY_HOST}:${REGISTRY_PORT}"]
capabilities = ["pull", "resolve"]
EOF
# Default mirror for all other registries
sudo mkdir -p "${CERTS_D}/_default"
sudo tee "${CERTS_D}/_default/hosts.toml" >/dev/null << EOF
server = "https://registry-1.docker.io"
[host."https://${REGISTRY_HOST}:${REGISTRY_PORT}"]
capabilities = ["pull", "resolve"]
EOF
sudo systemctl restart containerd
Most air-gapped registries use a self-signed or enterprise CA certificate. If
yours does, set a REGISTRY_CA variable alongside the others and add a
ca entry to each [host] block, as the BCM example does. Omit ca
only when a CA that the node already trusts signed the registry certificate.