Skip to content

Container Registry

LoKO includes a built-in OCI container registry (Zot) for local image storage and mirroring.

The registry is accessible at:

https://cr.${DOMAIN}

Example: https://cr.dev.me

Registry web UI (Explore page):

https://cr.${DOMAIN}/explore

Example: https://cr.dev.me/explore

Terminal window
# Tag image for local registry
docker tag myapp:latest cr.dev.me/myapp:latest
# Push to registry
docker push cr.dev.me/myapp:latest
Terminal window
# Pull from registry
docker pull cr.dev.me/myapp:latest
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: cr.dev.me/myapp:latest

No image pull secrets needed - registry runs in the cluster.

registry:
enabled: true
name: cr # Subdomain name
storage: 20Gi # PVC size
# renovate: datasource=helm depName=zot repositoryUrl=http://zotregistry.dev/helm-charts
version: "0.1.95" # Zot chart/app version

Default storage: 20Gi PersistentVolumeClaim

Adjust for your needs:

registry:
storage: 50Gi # Larger storage

Cache external registries locally for faster pulls:

registry:
enabled: true
mirroring:
enabled: true
sources:
- name: docker_hub
enabled: true
- name: quay
enabled: true
- name: ghcr
enabled: true
- name: k8s_registry
enabled: true
- name: mcr
enabled: true

Enable Zot CVE scanning (Trivy-backed) from config:

registry:
scanning:
enabled: true
interval: 24h
  • enabled: toggles extensions.search.cve in Zot config.
  • interval: Trivy DB update interval (12h, 24h, etc.).
%% title: Registry Mirroring Flow %%
graph LR
    K8s[Kubernetes] -->|1. Pull nginx| Registry[Local Registry]
    Registry -->|2. Cache Miss| Docker[Docker Hub]
    Docker -->|3. Return Image| Registry
    Registry -->|4. Cache & Serve| K8s
    K8s -->|5. Future Pulls| Registry
    Registry -.Fast local cache.-> K8s

    classDef k8sStyle fill:#3949ab,stroke:#283593,color:#fff
    classDef registryStyle fill:#00897b,stroke:#00695c,color:#fff
    classDef externalStyle fill:#fb8c00,stroke:#e65100,color:#fff

    class K8s k8sStyle
    class Registry registryStyle
    class Docker externalStyle

Images are automatically cached when pulled:

# In Kubernetes manifest
spec:
containers:
- name: nginx
image: docker.io/library/nginx:latest

First pull: fetches from Docker Hub Subsequent pulls: served from local cache

Terminal window
loko registry status

Shows:

  • Registry version
  • Storage capacity
  • Number of repositories
  • Configuration
Terminal window
loko registry list-repos

Output:

Local Repositories:
- myapp (2 tags)
- testimage (1 tag)
Mirrored Repositories:
- docker.io/library/nginx (3 tags)
- docker.io/library/postgres (2 tags)
Terminal window
loko registry show-repo myapp
loko registry show-repo docker.io/library/nginx
Terminal window
loko registry list-tags myapp

Output:

Tags for myapp:
- latest
- v1.0.0
- v0.9.0

Remove all tags from a specific repository (marks manifests for garbage collection):

Terminal window
loko registry purge-repo myapp
loko registry purge-repo myapp --force # skip confirmation

Delete all tags from every repository in the registry:

Terminal window
loko registry purge
loko registry purge --force # skip confirmation

Load a locally built Docker image directly into the Kind cluster nodes without pushing to the registry:

Terminal window
# Load a single image
loko registry load-image myapp:latest
# Load multiple images at once
loko registry load-image myapp:latest myapp:v1.0.0
# Load into specific nodes only
loko registry load-image myapp:latest --nodes kind-worker

This calls kind load docker-image under the hood and is useful during development when you want to test a local build without a push/pull cycle.

Terminal window
# Build image
docker build -t myapp:v1.0.0 .
# Tag for registry
docker tag myapp:v1.0.0 cr.dev.me/myapp:v1.0.0
docker tag myapp:v1.0.0 cr.dev.me/myapp:latest
# Push to registry
docker push cr.dev.me/myapp:v1.0.0
docker push cr.dev.me/myapp:latest
Terminal window
# Build multi-arch
docker buildx build --platform linux/amd64,linux/arm64 \
-t cr.dev.me/myapp:latest \
--push .
name: Build and Push
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push
run: |
docker build -t cr.dev.me/myapp:${{ github.sha }} .
docker push cr.dev.me/myapp:${{ github.sha }}
#!/bin/bash
# build-and-deploy.sh
VERSION=$(git rev-parse --short HEAD)
IMAGE="cr.dev.me/myapp:$VERSION"
# Build
docker build -t $IMAGE .
# Push
docker push $IMAGE
# Deploy
kubectl set image deployment/myapp myapp=$IMAGE

By default, the registry allows anonymous:

  • ✅ Push
  • ✅ Pull
  • ✅ List

Suitable for local development.

Edit registry configuration:

# Custom values for registry
components:
registry:
values:
httpAuth:
enabled: true
credentials:
- user: admin
password: secretpassword

Then configure Docker:

Terminal window
docker login cr.dev.me

Use registry for Docker build cache:

Terminal window
# Build with cache from registry
docker build \
--cache-from cr.dev.me/myapp:latest \
-t cr.dev.me/myapp:new \
.
# Push with cache
docker build \
--cache-to type=registry,ref=cr.dev.me/myapp:buildcache \
-t cr.dev.me/myapp:latest \
.

Zot includes a web interface:

https://cr.dev.me

Features:

  • Browse repositories
  • View tags
  • Search images
  • View image details
  • Delete tags (if authentication enabled)
Terminal window
# Check certificate trust
docker info | grep -i insecure
# Verify registry certificate
curl -v https://cr.dev.me/v2/
# Reinstall CA trust and Docker certs.d
loko certs ca install
Terminal window
# Check registry pod
kubectl get pods -n kube-system -l app=zot
# Check registry service
kubectl get svc -n kube-system zot
# Check registry logs
loko logs workload zot
Terminal window
# Check registry storage
kubectl get pvc -n kube-system
# Increase storage size
vim loko.yaml
# Update components.registry.storage.size
# Recreate environment
loko env recreate
Terminal window
# Verify mirroring config
yq '.environment.components.registry.mirroring' loko.yaml
# Check registry logs for errors
loko logs workload zot | grep -i mirror
# Test manual pull
docker pull cr.dev.me/docker.io/library/nginx:latest
Terminal window
# Check node containerd config
docker exec -it dev-me-worker cat /etc/containerd/config.toml
# Verify registry is in hosts.toml
docker exec -it dev-me-worker cat /etc/containerd/certs.d/cr.dev.me/hosts.toml
# Recreate cluster
loko env recreate
components:
registry:
port: 5000 # Custom port

Enable automatic garbage collection:

components:
registry:
values:
gc:
enabled: true
interval: "24h"
deleteUntagged: true

Use different storage backend:

components:
registry:
values:
storage:
type: s3
s3:
region: us-east-1
bucket: my-registry-bucket

Enable vulnerability scanning:

components:
registry:
values:
extensions:
search:
enable: true
cve:
updateInterval: "24h"

Monitor cache efficiency:

Terminal window
# Check mirrored repositories
loko registry list-repos | grep docker.io
# Compare with pulled images
kubectl get pods -A -o yaml | grep image:
Terminal window
# Check PVC usage
kubectl get pvc -n kube-system zot-storage
# Describe PVC for details
kubectl describe pvc -n kube-system zot-storage
  • Use semantic versioning for tags (v1.0.0)
  • Tag with both version and latest
  • Enable mirroring for frequently used images
  • Monitor storage usage
  • Clean up old tags regularly
  • Use :latest tag only (hard to track versions)
  • Store large unused images (wastes space)
  • Disable TLS verification (security risk)
  • Forget to push after building