πŸ“š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
ai intermediate ⏱ 20 minutes K8s 1.28+

Copy NVIDIA NIM Images to Internal Quay Reg...

Pull NIM container images from nvcr.io and push to an internal Quay registry. Covers authentication, tagging, air-gapped workflows, and curl token issues.

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

πŸ’‘ Quick Answer: Use podman pull from nvcr.io, retag for your internal Quay, then podman push. For API token endpoints, ensure your JSON has no escaped backslashes inside single quotes and use the correct field capitalization (GrantType not grantType).

The Problem

You need to deploy NVIDIA NIM containers (like DeepSeek-R1) on an OpenShift cluster that cannot pull directly from nvcr.io. Your organization uses an internal Quay registry, and all images must be promoted through a controlled pipeline. Along the way, you hit authentication issues with the platform API token endpoint and shell escaping problems with curl.

flowchart LR
    NGC["nvcr.io<br/>(NVIDIA NGC)"] -->|podman pull| WS["Workstation"]
    WS -->|podman tag| WS
    WS -->|podman push| QUAY["Internal Quay<br/>Registry"]
    QUAY -->|ImageStream| OCP["OpenShift<br/>Cluster"]

The Solution

Step 1: Authenticate to NVIDIA NGC

# Login to NVIDIA NGC registry
# Username is always $oauthtoken
# Password is your NGC API key from https://ngc.nvidia.com/setup
podman login nvcr.io
# Username: $oauthtoken
# Password: <your-ngc-api-key>
# Login Succeeded

Step 2: Authenticate to Internal Quay

# Login to your internal Quay registry
podman login quay-int.registry.example.com
# Username: your-username (or robot account)
# Password: <your-token>
# Login Succeeded

Step 3: Pull the NIM Image

# Pull the specific NIM image
podman pull nvcr.io/nim/deepseek-ai/deepseek-r1:latest

# Verify the image is present
podman images | grep deepseek
# nvcr.io/nim/deepseek-ai/deepseek-r1   latest   abc123...   11.5GB

⚠️ Pin by version, not latest β€” for production, always use a specific tag like 1.7.3 to avoid unexpected changes.

# Better: pull a pinned version
podman pull nvcr.io/nim/deepseek-ai/deepseek-r1:1.7.3

Step 4: Retag for Internal Registry

# Tag for your internal Quay repository
podman tag \
  nvcr.io/nim/deepseek-ai/deepseek-r1:1.7.3 \
  quay-int.registry.example.com/ai-models/deepseek-r1:1.7.3

Step 5: Push to Internal Quay

# Push to internal registry
podman push quay-int.registry.example.com/ai-models/deepseek-r1:1.7.3

# Verify in Quay UI or via API
podman search quay-int.registry.example.com/ai-models/deepseek-r1

Step 6: Create OpenShift ImageStream (Optional)

apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
  name: deepseek-r1
  namespace: ai-workloads
spec:
  lookupPolicy:
    local: true
  tags:
    - name: "1.7.3"
      from:
        kind: DockerImage
        name: quay-int.registry.example.com/ai-models/deepseek-r1:1.7.3
      importPolicy:
        scheduled: false
      referencePolicy:
        type: Local
oc apply -f imagestream-deepseek.yaml
oc get is deepseek-r1 -n ai-workloads

Bonus: Pin by Digest for Immutable References

# Get the exact digest
podman inspect nvcr.io/nim/deepseek-ai/deepseek-r1:1.7.3 \
  --format '{{.Digest}}'
# sha256:a1b2c3d4e5f6...

# Tag with digest-based version
podman tag \
  nvcr.io/nim/deepseek-ai/deepseek-r1@sha256:a1b2c3d4e5f6... \
  quay-int.registry.example.com/ai-models/deepseek-r1:1.7.3

podman push quay-int.registry.example.com/ai-models/deepseek-r1:1.7.3

Fixing curl Token Endpoint Issues

When authenticating to platform APIs (like Run:AI or other ML platforms), a common pattern is requesting an OAuth token via curl. Here are the most frequent pitfalls:

Problem 1: Trailing Spaces After Backslash

# ❌ WRONG β€” trailing space after backslash breaks line continuation
curl -X POST 'https://api.platform.example.com/api/v1/token' \Β·
  --header 'Content-Type: application/json' \Β·
  --data-raw '{"grantType":"client_credentials"}'
# bash: --header: command not found

The backslash (\) must be the very last character on the line β€” no trailing spaces.

# βœ… CORRECT β€” no spaces after backslash
curl -X POST 'https://api.platform.example.com/api/v1/token' \
  -H 'Content-Type: application/json' \
  -d '{"GrantType":"client_credentials","clientId":"my-client","clientSecret":"my-secret"}'

Problem 2: Backslashes Inside Single-Quoted JSON

# ❌ WRONG β€” backslashes inside single quotes become literal
curl -X POST 'https://api.platform.example.com/api/v1/token' \
  -d '{ \
  "GrantType":"client_credentials" \
}'
# Error: invalid character '\\' looking for beginning of object key string

Single quotes in bash already protect everything β€” never add backslashes inside them:

# βœ… CORRECT β€” clean JSON inside single quotes
curl -X POST 'https://api.platform.example.com/api/v1/token' \
  -H 'Content-Type: application/json' \
  -d '{
    "GrantType": "client_credentials",
    "clientId": "my-client",
    "clientSecret": "my-secret"
  }'

Problem 3: Case-Sensitive Field Names

Many OAuth endpoints are case-sensitive. grantType β‰  GrantType:

# ❌ May fail β€” wrong case
-d '{"grantType": "client_credentials"}'

# βœ… Check API docs for exact casing
-d '{"GrantType": "client_credentials"}'

Debug Tip: Verbose Mode

# Add -v to see exactly what curl sends
curl -v -X POST 'https://api.platform.example.com/api/v1/token' \
  -H 'Content-Type: application/json' \
  -d '{"GrantType":"client_credentials","clientId":"my-client","clientSecret":"my-secret"}'

# Look for:
# > Content-Type: application/json
# > {"GrantType":"client_credentials",...}
# If you see backslashes in the body β†’ your JSON is malformed

Automation Script: Bulk NIM Image Mirror

#!/bin/bash
# mirror-nim-images.sh β€” Pull NIM images from NGC and push to internal Quay
set -euo pipefail

INTERNAL_REGISTRY="quay-int.registry.example.com"
INTERNAL_ORG="ai-models"

# List of NIM images to mirror
IMAGES=(
  "nvcr.io/nim/deepseek-ai/deepseek-r1:1.7.3"
  "nvcr.io/nim/meta/llama-3.1-405b-instruct:1.5.2"
  "nvcr.io/nim/meta/llama-3.1-70b-instruct:1.5.2"
)

for IMAGE in "${IMAGES[@]}"; do
  # Extract name and tag
  NAME=$(echo "$IMAGE" | awk -F'/' '{print $NF}' | cut -d: -f1)
  TAG=$(echo "$IMAGE" | awk -F: '{print $NF}')
  TARGET="${INTERNAL_REGISTRY}/${INTERNAL_ORG}/${NAME}:${TAG}"

  echo "=== Mirroring ${IMAGE} β†’ ${TARGET} ==="

  # Pull
  podman pull "$IMAGE"

  # Tag
  podman tag "$IMAGE" "$TARGET"

  # Push
  podman push "$TARGET"

  echo "βœ… ${NAME}:${TAG} mirrored successfully"
done

echo "=== All images mirrored ==="

Common Issues

IssueCauseFix
denied: requested access on pushMissing Quay permissionsCreate repo first or grant robot account write access
manifest unknown on pullTag doesn’t existList available tags: podman search --list-tags nvcr.io/nim/...
x509: certificate signed by unknown authorityInternal Quay uses custom CAAdd CA to /etc/pki/ca-trust/ and run update-ca-trust
invalid character '\\' in curlBackslashes inside single-quoted JSONRemove all \ from inside '...' blocks
unsupported value '' for field 'GrantType'Broken line continuation or wrong caseFix trailing spaces after \, check field capitalization
Image too large for single pullMulti-GB NIM images (10GB+)Use --retry 3 flag, ensure sufficient disk space

Best Practices

  • Pin versions β€” never use latest in production; pin to specific tags like 1.7.3
  • Pin by digest β€” for immutable references, use @sha256:... notation
  • Use robot accounts β€” create dedicated Quay robot accounts for CI/CD image mirroring
  • Mirror on schedule β€” automate with CronJobs or Tekton pipelines to keep images current
  • Verify after push β€” always check the Quay UI or use skopeo inspect to confirm
  • Use skopeo for air-gapped β€” skopeo copy can transfer between registries without pulling locally
  • Clean up local images β€” remove pulled images after push to save disk: podman rmi $IMAGE

Key Takeaways

  • Pull NIM images from nvcr.io with your NGC API key, retag, and push to internal Quay
  • Always pin image versions for production deployments
  • When using curl for API tokens: no trailing spaces after \, no backslashes inside single quotes, and check field name casing
  • Use skopeo copy for direct registry-to-registry transfer without local storage
  • Create OpenShift ImageStreams to reference internal registry images cleanly
#nvidia-nim #quay-registry #container-images #air-gapped #podman
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