Kubernetes operator for declarative registry-to-registry image sync
Quick Start • Configuration • Deploy Guide • Metrics
Portager is a Kubernetes operator that declaratively syncs container images between OCI-compliant registries. Define an ImageSync custom resource specifying source images, a destination registry, a cron schedule, and authentication — the operator handles the rest.
The name comes from the act of carrying cargo between two bodies of water, which is exactly what this does: carrying container images between two registries.
You create an ImageSync CR. The operator compares source and destination digests on your cron schedule, copies only what's changed, and reports status back on the resource. No pipelines, no scripts — just a CRD and a controller.
┌──────────────┐
│ ImageSync │
│ CRD │
└──────┬───────┘
│
▼
┌─────────┐ ┌────────────────┐ ┌─────────┐
│ Source │────▶│ Portager │────▶│ Dest │
│ Registry│ │ Controller │ │ Registry│
└─────────┘ └────────────────┘ └─────────┘
Docker Hub Digest compare ECR
GHCR Copy if changed GCR
Chainguard Skip if matching Harbor
Quay Update status Nexus
There is no CRD-native operator in the Kubernetes ecosystem for declarative registry-to-registry image relocation. The existing alternatives each fall short:
| Alternative | Limitation |
|---|---|
| ECR pull-through cache | AWS-only, no support for arbitrary private registries (e.g., Chainguard cgr.dev), unavailable in GovCloud |
| dregsy | Config-file driven, not CRD-driven — no kubectl get imagesync |
| kube-image-keeper (kuik) | Caches to an in-cluster registry only; does not push to external registries |
| Flux image automation | Watches for new tags and updates Git manifests; does not copy images |
| CI/CD pipelines | Couples relocation to CI availability; no Kubernetes-native status reporting |
Portager fills this gap with a Kubernetes-native, GitOps-friendly approach that works with any OCI-compliant registry.
- Declarative — Define image sync rules as Kubernetes custom resources
- Digest-based skip — Compares manifest digests via HTTP HEAD; skips unchanged images without downloading layers
- Registry-agnostic — Works with Docker Hub, GHCR, Quay, Chainguard, ECR, GCR, Harbor, Nexus, and any OCI-compliant registry
- Pluggable auth — Kubernetes Secrets (
dockerconfigjson), ECR via IRSA, or anonymous for public registries - Cron scheduling — Standard cron expressions, shorthands like
@every 6h, and on-demand sync via annotation - Observable — Per-image status on the resource, Kubernetes Events, and custom Prometheus metrics
- Kubernetes 1.28+
- Helm v3+
helm install portager oci://ghcr.io/jarodr47/portager/charts/portager \
--version 0.1.0 -n portager-system --create-namespaceInstall with Kustomize
git clone https://github.com/jarodr47/portager.git
cd portager
make install # Install CRDs
make deploy # Deploy controller + RBACkubectl get pods -n portager-system
# NAME READY STATUS AGE
# portager-controller-manager-xxxxxxxxxx-xxxxx 1/1 Running 30s# base-images.yaml
apiVersion: portager.portager.io/v1alpha1
kind: ImageSync
metadata:
name: base-images
namespace: default
spec:
schedule: "@every 6h"
source:
registry: docker.io/library
destination:
registry: 123456789012.dkr.ecr.us-east-1.amazonaws.com
auth:
method: ecr
repositoryPrefix: mirror
createDestinationRepos: true
images:
- name: alpine
tags: ["3.21", "latest"]
- name: nginx
tags: ["1.27", "latest"]kubectl apply -f base-images.yaml
kubectl describe imagesync base-imagesEvents:
Type Reason Message
---- ------ -------
Normal RepoEnsured ECR repository "mirror/alpine" exists or was created
Normal ImageSynced Synced docker.io/library/alpine:3.21 -> ECR (digest: sha256:c3f8e73f)
Normal SyncComplete Sync complete: 4 synced, 0 failed, 4 total
For full deployment walkthroughs — including EKS with IRSA, non-EKS clusters, and private source registries — see the Deploy Guide.
For Helm values, ImageSync spec reference, auth strategies, and metrics, see Configuration.
kubectl delete imagesync --all -A
helm uninstall portager -n portager-system
kubectl delete crd imagesyncs.portager.portager.io
kubectl delete ns portager-systemUninstall with Kustomize
kubectl delete imagesync --all -A
make undeploy
make uninstallmake build # Build the controller binary
make test # Run unit and integration tests
make lint # Run golangci-lint
make helm-lint # Lint the Helm chartSee Configuration — Development for the full local development setup.
Licensed under the Apache License, Version 2.0. See LICENSE for details.