image-sync 0.1.1

Smart container image cache synchronizer — pulls from external registries only when needed
image-sync-0.1.1 is not a library.

image-sync

Smart container image cache synchronizer -- pulls from external registries only when the remote digest differs from what is cached locally. Designed to run as a Kubernetes CronJob alongside a Zot registry to eliminate Docker Hub rate limits during scale testing.

How It Works

For each image in config:
  1. crane digest <source>:<tag>          # HEAD request only, no pull
  2. crane digest <cache>/<path>:<tag>    # check local Zot
  3. If digests differ (or not cached):
       crane copy <source>:<tag> <cache>/<path>:<tag>
  4. If identical: skip (no API calls wasted)

This digest-first approach means the CronJob can run frequently (every 10 minutes) without wasting Docker Hub API quota on images that have not changed.

Config Format

YAML config loaded from --config flag, IMAGE_SYNC_CONFIG env var, or /etc/image-sync/config.yaml (default for K8s CronJob mount).

cache_registry: "image-cache.image-cache.svc.cluster.local:5000"

images:
  - source: "docker.io/akeyless/k8s-secrets-sidecar"
    tag: "0.35.1"
  - source: "docker.io/akeyless/k8s-webhook-server"
    tag: "latest"
  - source: "docker.io/library/nginx"
    tag: "1.27-alpine"
  - source: "ghcr.io/project-zot/zot-linux-amd64"
    tag: "v2.1.2"
    cache_as: "project-zot/zot-linux-amd64"  # optional path override

settings:
  concurrency: 2
  pull_timeout_secs: 300
  skip_on_error: false  # set true to not fail the CronJob on partial failures

Docker Hub Authentication

For Docker Hub images, mount a Docker config secret with your credentials:

# K8s CronJob env
- name: DOCKER_CONFIG
  value: /tmp/docker

# Volume mount
volumes:
  - name: docker-auth
    secret:
      secretName: dockerhub-auth

The dockerhub-auth secret should contain a .dockerconfigjson file with a Docker Hub personal access token. Without auth, Docker Hub allows only 100 pulls per 6 hours from anonymous IPs.

Output

Writes a JSON report to stdout:

{
  "timestamp": "2026-03-30T...",
  "cache_registry": "image-cache.image-cache.svc.cluster.local:5000",
  "summary": {
    "pulled": 1,
    "already_cached": 4,
    "failed": 0,
    "total": 5
  },
  "results": [
    {
      "image": "docker.io/akeyless/k8s-secrets-sidecar",
      "tag": "0.35.1",
      "action": "already_cached",
      "remote_digest": "sha256:abc...",
      "cached_digest": "sha256:abc...",
      "duration_ms": 320,
      "error": null
    }
  ]
}

Container Image

Published at ghcr.io/pleme-io/image-sync (multi-arch: amd64, arm64). Built with substrate's rust-tool-image-flake.nix pattern -- the Docker image includes crane on PATH for registry operations.

# Build locally
nix build

# Build and push Docker images
nix run .#release

# Run locally
nix run -- --config config.yaml --dry-run

Runtime Dependencies

  • crane (included in Docker image via extraContents)
  • skopeo (fallback, used if crane fails)
  • Network access to source registries and the cache registry

Kubernetes Deployment

Runs as a CronJob in the image-cache namespace. See k8s/clusters/scale-test/infrastructure/image-sync/ for the full manifest set:

  • cronjob.yaml -- runs every 10 minutes with Forbid concurrency
  • configmap.yaml -- image-sync YAML config
  • dockerhub-secret.enc.yaml -- SOPS-encrypted Docker Hub credentials
  • job-initial-sync.yaml -- one-shot Job for initial cache population