c9s
Guides

Image pulling

Configure public, private, pull-through, proxied, and air-gapped image workflows.

This guide explains how to configure image pulling in Clabernetes, including private registries, pull secrets, and pull-through modes.

Overview

Clabernetes launcher pods run containerlab, which in turn runs Docker to manage network device containers. Images can be pulled in several ways:

  1. Direct pull: Docker in the launcher pulls images directly
  2. Pull-through: Clabernetes pre-pulls images via the cluster CRI

Pull-Through Modes

Auto (Default)

Clabernetes automatically detects if pull-through is needed:

spec:
  imagePull:
    pullThroughOverride: auto

Behavior:

  • Checks if image exists in launcher's Docker
  • If not, requests pull via ImageRequest CRD
  • Controller creates a pull pod on the same node
  • Image is pulled to node's CRI, then available to Docker

Always

Force pull-through for all images:

spec:
  imagePull:
    pullThroughOverride: always

Use cases:

  • Private registries requiring cluster credentials
  • Ensuring images are cached at CRI level
  • Consistent behavior across all topologies

Never

Disable pull-through, use Docker direct pull:

spec:
  imagePull:
    pullThroughOverride: never

Use cases:

  • Public images that Docker can pull directly
  • When pull-through isn't working
  • Debugging image pull issues

Private Registry Configuration

Using Pull Secrets

Create a Kubernetes secret for your registry:

kubectl create secret docker-registry my-registry-secret \
  --docker-server=registry.example.com \
  --docker-username=myuser \
  --docker-password=mypass \
  --docker-email=myemail@example.com

Reference in your topology:

apiVersion: c9s.run/v1alpha1
kind: Topology
metadata:
  name: private-images
spec:
  imagePull:
    pullThroughOverride: always
    pullSecrets:
      - my-registry-secret
  definition:
    containerlab: |
      name: private
      topology:
        nodes:
          srl1:
            kind: nokia_srlinux
            image: registry.example.com/nokia/srlinux:latest

How it works:

  1. Controller creates ImageRequest for each image
  2. Pull pod is created with the specified pull secrets
  3. Image is pulled to node's CRI
  4. Launcher can then use the image

Using Docker Config

For more complex authentication (multiple registries, credential helpers):

# Create secret from existing docker config
kubectl create secret generic docker-config \
  --from-file=config.json=$HOME/.docker/config.json

Reference in topology:

spec:
  imagePull:
    dockerConfig: docker-config  # Secret name

The secret must contain a config.json key with valid Docker config.

Docker Daemon Configuration

For daemon-level settings (insecure registries, mirrors):

# Create secret with daemon.json
kubectl create secret generic docker-daemon-config \
  --from-file=daemon.json=/path/to/daemon.json

Example daemon.json:

{
  "insecure-registries": ["registry.local:5000"],
  "registry-mirrors": ["https://mirror.example.com"]
}

Reference in topology:

spec:
  imagePull:
    dockerDaemonConfig: docker-daemon-config

Insecure Registries

For registries without valid TLS:

spec:
  imagePull:
    insecureRegistries:
      - registry.local:5000
      - 10.0.0.100:5000

Note: This is ignored if dockerDaemonConfig is set (configure in daemon.json instead).

HTTP(S) Proxy Support

If your cluster reaches the internet through an HTTP(S) proxy, set the standard proxy env vars on the launcher pods -- either per topology or globally:

spec:
  deployment:
    extraEnv:
      - name: HTTP_PROXY
        value: http://proxy.example.com:8080
      - name: HTTPS_PROXY
        value: http://proxy.example.com:8080
      - name: NO_PROXY
        value: 10.96.0.0/16,10.244.0.0/16,.svc,.svc.cluster.local,localhost,127.0.0.1

Or for all topologies via the Config CRD (or the globalConfig.deployment.extraEnv helm value):

apiVersion: c9s.run/v1alpha1
kind: Config
metadata:
  name: clabernetes
spec:
  deployment:
    extraEnv:
      - name: HTTPS_PROXY
        value: http://proxy.example.com:8080
      # ...

How it works:

  • The launcher writes the proxy env vars into its Docker daemon config (proxies section), so direct Docker pulls go through the proxy.
  • Pull-through mode also works: the pull pod uses the node CRI (configure your CRI for the proxy as usual, most distributions handle this already), and the launcher-side nerdctl operations inherit the env vars.
  • The in-cluster Kubernetes API service address is automatically appended to NO_PROXY by the launcher, so it never tries to reach the API through the proxy.

Note: NO_PROXY should contain your cluster's service and pod CIDRs plus cluster-local DNS suffixes (see the example above -- adjust the CIDRs for your cluster). Proxy env vars are ignored if dockerDaemonConfig is set (configure proxies in your daemon.json instead).

Global Configuration

Set defaults in the Config CRD:

apiVersion: c9s.run/v1alpha1
kind: Config
metadata:
  name: clabernetes
spec:
  imagePull:
    # Default pull-through mode
    pullThroughOverride: auto
    # CRI socket for K3s
    criSockOverride: /run/k3s/containerd/containerd.sock
    # Default docker config for all topologies
    dockerConfig: global-docker-config
    dockerDaemonConfig: global-daemon-config

CRI Socket Override

For non-standard CRI socket locations (e.g., K3s):

spec:
  imagePull:
    criSockOverride: /run/k3s/containerd/containerd.sock

Common paths:

  • Standard containerd: /run/containerd/containerd.sock
  • K3s: /run/k3s/containerd/containerd.sock
  • Minikube: /var/run/containerd/containerd.sock

CRI Registry Hosts

Some containerd installations keep registry mirror, TLS, and host configuration outside the default /etc/containerd/certs.d directory. Set criHostsDir to make that host directory available to all pull-through launchers:

spec:
  imagePull:
    pullThroughOverride: always
    criHostsDir: /path/to/containerd/hosts

The directory is mounted read-only at both its original path and /etc/containerd/certs.d. Keeping the original path available allows certificate paths rooted inside that directory tree to continue working, while the conventional path lets nerdctl use the same registry configuration as the node runtime. Absolute certificate paths outside criHostsDir are not mounted; keep those certificates under criHostsDir or make them available to the launcher separately. The configured path must be an existing directory on every containerd node that can run a pull-through launcher. It is not mounted when pull-through is disabled or the effective CRI kind is not containerd; a configured criKindOverride takes precedence over cluster auto-detection.

Complete Examples

Public Registry

apiVersion: c9s.run/v1alpha1
kind: Topology
metadata:
  name: public-images
spec:
  imagePull:
    pullThroughOverride: auto
  definition:
    containerlab: |
      name: public
      topology:
        nodes:
          srl1:
            kind: nokia_srlinux
            image: ghcr.io/nokia/srlinux:latest

Private Registry with Pull Secrets

apiVersion: c9s.run/v1alpha1
kind: Topology
metadata:
  name: enterprise
spec:
  imagePull:
    pullThroughOverride: always
    pullSecrets:
      - enterprise-registry-creds
  definition:
    containerlab: |
      name: enterprise
      topology:
        nodes:
          srl1:
            kind: nokia_srlinux
            image: registry.corp.example.com/network/srlinux:23.10.1

Insecure Local Registry

apiVersion: c9s.run/v1alpha1
kind: Topology
metadata:
  name: local-dev
spec:
  imagePull:
    pullThroughOverride: never
    insecureRegistries:
      - localhost:5000
  definition:
    containerlab: |
      name: local
      topology:
        nodes:
          srl1:
            kind: nokia_srlinux
            image: localhost:5000/srlinux:dev

Air-Gapped Environment

apiVersion: c9s.run/v1alpha1
kind: Topology
metadata:
  name: airgapped
spec:
  imagePull:
    pullThroughOverride: always
    pullSecrets:
      - internal-registry-secret
    dockerConfig: docker-auth-config
  definition:
    containerlab: |
      name: airgapped
      topology:
        nodes:
          srl1:
            kind: nokia_srlinux
            image: internal-registry.corp:5000/nokia/srlinux:latest

Troubleshooting

Image Pull Failures

Check ImageRequest status:

kubectl get imagerequests
kubectl describe imagerequest <name>

Check pull pod:

kubectl get pods -l c9s.run/pullerImageHash=<image-hash>
kubectl logs <pull-pod-name>

Authentication Issues

Verify secret exists and is correct:

kubectl get secret my-registry-secret -o yaml

Test manually:

kubectl run test --rm -it --image=<your-image> \
  --overrides='{"spec":{"imagePullSecrets":[{"name":"my-registry-secret"}]}}'

CRI Socket Issues

Verify socket path:

# On the node
ls -la /run/containerd/containerd.sock

Check launcher logs:

kubectl logs -l c9s.run/topologyNode=<node> -c clabernetes-launcher

Pull-Through Not Working

  1. Check ImageRequest is created and accepted
  2. Verify pull pod starts and completes
  3. Check CRI has the image: crictl images | grep <image>
  4. Verify Docker can see the image in launcher

Best Practices

  1. Use pull-through for private registries: Leverages Kubernetes secrets properly
  2. Pre-pull large images: Reduces topology startup time
  3. Use registry mirrors: For faster pulls in large clusters
  4. Set appropriate pull policies: IfNotPresent for stability, Always for latest
  5. Secure your secrets: Use RBAC to protect registry credentials

Configuration Priority

  1. The one explicitly referenced LauncherProfile (or the shared profile generated from Topology-level imagePull settings)
  2. Global Config CRD imagePull settings for fields the profile omits
  3. Default behavior (auto pull-through)

LauncherProfiles are attached through Node.spec.launcherProfileRef; there is no label selector or multi-profile priority chain.

On this page