Decision. Self-host the registry
Alternative. Docker Hub or another hosted registry
Why. No pull rate limits, images stay on the LAN for fast pulls, and the cluster remains fully self-contained.
Private container registry that scans images for known vulnerabilities
Zot is the private OCI registry for every image built in-repo. Project builds push multi-arch images here, and ArgoCD-managed apps pull from it. It also scans images for known vulnerabilities.
Excerpt from argocd-apps/values.yaml in the argocd-apps chart,
with annotations added for this site:
zot:
# true
application: true
project: cluster-services
# false
autoSync: false
#redis:
# application: true
# project: cluster-services
# autoSync: false
The full zot/values.yaml from the service's
own chart:
zot:
replicaCount: 1
image:
repository: ghcr.io/project-zot/zot
pullPolicy: IfNotPresent
tag: "v2.1.20"
serviceAccount:
create: true
name: "zot-service-account"
serviceHeadless:
enabled: false
port: 5000
service:
type: ClusterIP
port: 5000
ingress:
enabled: false
httproute:
enabled: true
annotations:
link.argocd.argoproj.io/external-link: "https://registry.opi5cluster.co.uk"
parentRefs:
- name: istio-gateway
namespace: istio
sectionName: websecure
hostnames:
- registry.opi5cluster.co.uk
pathType: PathPrefix
path: /
rules:
- backendRefs:
- name: zot
port: 5000
timeouts:
request: 0s
backendRequest: 0s
httpGet:
scheme: HTTP
port: 5000
startupProbe:
initialDelaySeconds: 20
periodSeconds: 60
failureThreshold: 10
# If mountConfig is true the configMap named $CHART_RELEASE-config is mounted
# on the pod's '/etc/zot' directory
mountConfig: true
configFiles:
config.json: |-
{
"storage": {
"rootDirectory": "/var/lib/registry",
"gc": true
},
"log": { "level": "debug" },
"http": {
"address": "0.0.0.0",
"port": "5000",
"compat": ["docker2s2"]
},
"extensions": {
"ui": {
"enable": true
},
"sync": {
"enable": true,
"credentialsFile": "/secrets/credentials.json",
"registries": [
{
"urls": ["https://registry.gitlab.com"],
"content": [
{
"prefix": "opi5-cluster/**",
"destination": "/",
"stripPrefix": true
}
],
"onDemand": true,
"tlsVerify": true
},
{
"urls": ["https://index.docker.io"],
"content": [
{
"prefix": "metabase/metabase",
"tags": {
"regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$",
"semver": true
},
"destination": "/metabase",
"stripPrefix": true
}
],
"onDemand": true,
"tlsVerify": true
}
]
},
"search": {
"enable": true,
"cve": {
"updateInterval": "2h",
"trivy": {
"dbRepository": "registry.opi5cluster.co.uk/aquasecurity/trivy-db",
"javaDbRepository": "registry.opi5cluster.co.uk/aquasecurity/trivy-java-db"
}
}
},
"metrics": {
"enable": true,
"prometheus": {
"path": "/metrics"
}
}
}
}
externalSecrets:
- secretName: registry-credentials
mountPath: /secrets
# If persistence is 'true' the service uses a persistentVolumeClaim to mount a
# volume for zot on '/var/lib/registry'; by default the pvc used is named
# '$CHART_RELEASE-pvc', but the name can be changed below
persistence: true
# PVC data, only used if persistence is 'true'
pvc:
create: true
name: zot-pvc
accessModes: ["ReadWriteOnce"]
storage: 500Gi
storageClassName: longhorn-ssd-large
# Persistent cache for the Trivy vulnerability database. Trivy backs zot's
# CVE scanning; without a stable cache, the ~150MB DB is re-fetched on
# every pod restart and scans can race while it's empty. The PVC is
# created by templates/trivy-cache-pvc.yaml in this chart, then mounted
# into the upstream zot pod via extraVolumes/extraVolumeMounts below.
trivyCache:
enabled: true
# Path inside the container; also exported as $TRIVY_CACHE_DIR so trivy
# writes here regardless of its compiled-in default.
mountPath: /tmp/trivy
pvc:
# PVC name. Must match the claimName in extraVolumes below.
# Defaults to "zot-trivy-cache".
name: null
accessModes: ["ReadWriteOnce"]
# trivy-db is ~150 MB today; 5 Gi leaves room for historical revisions.
storage: 5Gi
storageClassName: longhorn-ssd-large
# Mount the trivy cache PVC into the zot container. The volume name
# ("trivy-cache") and mountPath must match trivyCache.mountPath above.
extraVolumes:
- name: trivy-cache
persistentVolumeClaim:
claimName: zot-trivy-cache
extraVolumeMounts:
- name: trivy-cache
mountPath: /tmp/trivy
# Container env vars. We add TRIVY_CACHE_DIR so trivy's DB lives on the PVC.
env:
- name: TRIVY_CACHE_DIR
value: /tmp/trivy
# Deployment strategy type
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
# Extra args to pass to the deployment's container
extraArgs: []
podAnnotations: {}
podLabels: {}
deploymentAnnotations: {}
priorityClassName: ""
dnsConfig: {}
dnsPolicy: "ClusterFirst"
metrics:
enabled: true templates/registry-secret.yaml Image pull secret for authenticating against Zot.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: registry-external-secret
namespace: {{ .Release.Namespace }}
spec:
refreshInterval: 24h
secretStoreRef:
kind: ClusterSecretStore
name: vault-cluster-secret-store
target:
name: registry-credentials
creationPolicy: Owner
template:
type: Opaque
engineVersion: v2
data:
credentials.json: |
{
"registry.gitlab.com": {
"username": "{{ `{{ .gitlabUserName }}` }}",
"password": "{{ `{{ .gitlabUserPass }}` }}"
}
}
data:
- secretKey: gitlabUserName
remoteRef:
key: gitlab
property: ci_user
- secretKey: gitlabUserPass
remoteRef:
key: gitlab
property: ci_password templates/trivy-cache-pvc.yaml PVC holding the Trivy vulnerability database cache.
{{- if .Values.zot.trivyCache.enabled }}
{{- $pvcName := default "zot-trivy-cache" .Values.zot.trivyCache.pvc.name }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ $pvcName }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: zot
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: trivy-cache
spec:
accessModes:
{{- toYaml .Values.zot.trivyCache.pvc.accessModes | nindent 4 }}
resources:
requests:
storage: {{ .Values.zot.trivyCache.pvc.storage }}
{{- if .Values.zot.trivyCache.pvc.storageClassName }}
storageClassName: "{{ .Values.zot.trivyCache.pvc.storageClassName }}"
{{- end }}
{{- end }} Decision. Self-host the registry
Alternative. Docker Hub or another hosted registry
Why. No pull rate limits, images stay on the LAN for fast pulls, and the cluster remains fully self-contained.