Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.coreweave.com/llms.txt

Use this file to discover all available pages before exploring further.

Deploy Zot, an OCI-native container registry, on CoreWeave Kubernetes Service (CKS). Store container images in CoreWeave AI Object Storage and use LOTA as the S3 endpoint for faster reads within the cluster.
OCI conformanceCoreWeave has tested that the zot registry has passed the OCI Distribution Specification conformance suite, validating its compliance with the Open Container Initiative standards on our cloud. The conformance suite tests core registry operations including push and pull workflows, content discovery, and content management. This certification ensures Zot works reliably with OCI-compatible tooling and clients across the container ecosystem.

What you’ll use

This tutorial uses:

What you’ll do

In this tutorial, you will:
  1. Install cert-manager to issue a TLS certificate for the registry
  2. Create registry credentials for authenticating with the registry
  3. Issue a TLS certificate for the registry hostname
  4. Deploy Zot using Helm with S3-backed storage via LOTA, a public LoadBalancer Service, and the TLS certificate mounted into the pod
  5. Push and pull a container image to verify the registry

Prerequisites

Before you start, you must have: You’ll need the following tools on your local machine:

Verify cluster access

Verify that you can access your cluster with kubectl:
kubectl cluster-info
You should see something similar to:
Kubernetes control plane is running at...
CoreDNS is running at...
node-local-dns is running at...
Verify your cluster has at least one CPU Node:
kubectl get nodes -o=custom-columns="NAME:metadata.name,CLASS:metadata.labels['node\.coreweave\.cloud\/class']"
You should see at least one Node with cpu in the CLASS column:
NAME      CLASS
g5424e0   cpu
gd926d4   gpu

Install cert-manager

This tutorial uses the CoreWeave cert-manager Helm chart to issue a TLS certificate for the registry. Add the CoreWeave Helm repository:
helm repo add coreweave https://charts.core-services.ingress.coreweave.com
helm repo update
Install cert-manager:
helm upgrade --install cert-manager coreweave/cert-manager \
  --namespace cert-manager \
  --create-namespace
Enable the cert-issuers subchart to create Let’s Encrypt ClusterIssuers:
helm upgrade cert-manager coreweave/cert-manager \
  --namespace cert-manager \
  --set cert-issuers.enabled=true
Verify the ClusterIssuers are ready:
kubectl get clusterissuer
You should see:
NAME                        READY   AGE
letsencrypt-prod            True    60s
letsencrypt-staging         True    60s
selfsigned-cluster-issuer   True    60s

Determine the registry hostname

CoreWeave’s External Hostname Controller allocates an FQDN under .coreweave.app for any public LoadBalancer Service annotated with service.beta.kubernetes.io/external-hostname. For more details, see Create a public DNS name. Your registry hostname will follow this pattern:
registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app
Replace:
  • [ORG-ID] with your organization ID.
  • [CLUSTER-NAME] with the name of the target CKS cluster.
For example, if your Org ID is abc123 and your cluster is named prod, your registry hostname is registry.abc123-prod.coreweave.app. Note this hostname for the steps below.
If you already have any public LoadBalancer Service running in the cluster, you can inspect its ExternalRecords status condition to confirm the DNS suffix:
kubectl get svc [SERVICE-NAME] -n [NAMESPACE] \
  -o=jsonpath='{.status.conditions[?(@.type=="ExternalRecords")].message}'

Create a storage bucket

Create an AI Object Storage bucket in the Cloud Console. Note the bucket name for later use.

Generate access credentials

Create an Access Key for the bucket in the Cloud Console. Save the Access Key ID and Secret Key. You’ll need both when configuring Zot’s storage driver.

Create registry credentials

Generate a bcrypt-hashed password for the registry admin user. Replace your-username and your-password with your chosen credentials:
export REGISTRY_USER="your-username"
export REGISTRY_PASS="your-password"
export HTPASSWD=$(htpasswd -nbB "$REGISTRY_USER" "$REGISTRY_PASS")
If htpasswd is not installed, install it with apt-get install apache2-utils on Debian/Ubuntu or brew install httpd on macOS.
This tutorial uses htpasswd for simplicity. Zot also supports LDAP, mutual TLS, bearer token (OAuth2), and OpenID Connect with providers like GitHub, Google, and GitLab.
Create a Kubernetes Secret containing the htpasswd file:
kubectl create secret generic registry-htpasswd \
  --from-literal=htpasswd="$HTPASSWD"

Issue a TLS certificate

Create a Certificate resource so cert-manager issues a TLS certificate for your registry hostname using the bundled letsencrypt-prod ClusterIssuer. Save the following as registry-cert.yaml. Replace registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app with the hostname from the previous step:
registry-cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: registry-cert
spec:
  secretName: registry-cert
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app
Apply the manifest:
kubectl apply -f registry-cert.yaml
Wait for the certificate to be ready. Issuance typically completes within one to two minutes:
kubectl wait --for=condition=Ready certificate/registry-cert --timeout=5m
You should see:
certificate.cert-manager.io/registry-cert condition met
Confirm the certificate and its backing Secret exist:
kubectl get certificate registry-cert
kubectl get secret registry-cert
The Secret contains tls.crt and tls.key keys, which you’ll mount into the Zot pod in the next step.

Deploy Zot

Add the Zot Helm repository:
helm repo add zotregistry http://zotregistry.dev/helm-charts
helm repo update
Create a zot-values.yaml file. Replace the placeholder values with your own:
zot-values.yaml
replicaCount: 1

strategy:
  type: Recreate

serviceAccount:
  create: true

service:
  type: LoadBalancer
  port: 443
  annotations:
    service.beta.kubernetes.io/external-hostname: registry
    service.beta.kubernetes.io/coreweave-load-balancer-type: public

ingress:
  enabled: false

httpGet:
  scheme: HTTPS
  port: 5000

extraVolumes:
  - name: registry-tls
    secret:
      secretName: registry-cert

extraVolumeMounts:
  - name: registry-tls
    mountPath: /tls
    readOnly: true

persistence: false

mountConfig: true
configFiles:
  config.json: |
    {
      "storage": {
        "rootDirectory": "/tmp/zot",
        "dedupe": false,
        "storageDriver": {
          "name": "s3",
          "rootdirectory": "/zot",
          "region": "[REGION]",  # Replace with your bucket's region (e.g., us-east-14a)
          "bucket": "[BUCKET-NAME]",  # Replace with your AI Object Storage bucket name
          "regionendpoint": "http://cwlota.com",
          "accesskey": "[ACCESS-KEY-ID]",  # Replace with your Access Key ID
          "secretkey": "[SECRET-KEY]",  # Replace with your Secret Key
          "forcepathstyle": false,
          "secure": false,
          "skipverify": false
        }
      },
      "http": {
        "address": "0.0.0.0",
        "port": "5000",
        "tls": {
          "cert": "/tls/tls.crt",
          "key": "/tls/tls.key"
        },
        "compat": ["docker2s2"],
        "auth": {
          "htpasswd": {
            "path": "/secret/htpasswd"
          }
        },
        "accessControl": {
          "adminPolicy": {
            # Replace [USERNAME] with the registry admin username from the credentials step
            "users": ["[USERNAME]"],
            "actions": ["read", "create", "update", "delete"]
          }
        }
      },
      "log": { "level": "info" }
    }

mountSecret: true
secretFiles:
  # Replace [HTPASSWD-STRING] with the $HTPASSWD value from the credentials step
  htpasswd: [HTPASSWD-STRING]
Replace the following placeholders:
PlaceholderValue
[REGION]The region of your AI Object Storage bucket (for example, us-east-14a)
[BUCKET-NAME]Your AI Object Storage bucket name
[ACCESS-KEY-ID]Your AI Object Storage Access Key ID
[SECRET-KEY]Your AI Object Storage Secret Key
[USERNAME]The registry admin username from the credentials step
[HTPASSWD-STRING]The $HTPASSWD value generated in the credentials step
Configuration notes:
  • The service block creates a public LoadBalancer Service. The external-hostname: registry annotation tells the External Hostname Controller to allocate registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app for the Service. The coreweave-load-balancer-type: public annotation provisions a public IP. The Service listens on port 443 and forwards to the container’s zot named port, which is set by httpGet.port (see below).
  • The ingress.enabled: false field disables the chart’s Ingress resource. External clients connect directly to the LoadBalancer Service, so no ingress controller is required.
  • The httpGet block sets the probe scheme to HTTPS and the container port to 5000. The chart uses these values for its startup probe and to name the container port that the Service targets. The scheme must be HTTPS because Zot terminates TLS on this port; probing with HTTP would fail.
  • The extraVolumes and extraVolumeMounts fields mount the registry-cert Secret (created by cert-manager in the previous step) at /tls inside the Zot container. The http.tls block in the Zot config points to tls.crt and tls.key in that directory, so Zot terminates TLS natively.
  • The region must match where your AI Object Storage bucket was created. Find this in the Cloud Console on the bucket details page.
  • The regionendpoint is set to http://cwlota.com, the LOTA endpoint. LOTA caches object storage reads on local Node disks for faster access. The secure field is false because LOTA uses HTTP within the cluster. To access the bucket from outside a CKS cluster, use https://cwobject.com as the endpoint and set secure to true.
  • The compat: ["docker2s2"] field enables Docker v2 manifest support. Without this, docker push will fail because Zot rejects Docker v2 manifests by default. Remove this field if your workflow exclusively uses OCI-format images.
Install the chart:
helm upgrade --install zot zotregistry/zot \
  --version 0.1.98 \
  --atomic \
  --timeout 120s \
  -f zot-values.yaml
cert-manager renews the registry-cert Secret automatically before expiry. Because the Secret is mounted into the Zot pod, the updated certificate is available on disk after rotation. Restart the Zot pod to pick up the renewed certificate if Zot does not reload it automatically.
Verify the pod is running:
kubectl get pods -l app.kubernetes.io/name=zot
You should see:
NAME                   READY   STATUS    RESTARTS   AGE
zot-7b8f9d4c5f-xxxxx   1/1     Running   0          30s
Verify the LoadBalancer Service has an external IP and the expected public FQDN:
kubectl get svc zot
kubectl get svc zot -o=jsonpath='{.status.conditions[?(@.type=="ExternalRecords")].message}'
The EXTERNAL-IP column should show a public IP address, and the second command should print registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app. DNS propagation typically completes within one to two minutes.

Verify the registry

Log in to your registry using Docker. Replace [ORG-ID] and [CLUSTER-NAME] with the values for your cluster, and use the username and password you created in the Create registry credentials step:
docker login registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app -u $REGISTRY_USER
When prompted, enter the password from REGISTRY_PASS in the Create registry credentials section (or use -p $REGISTRY_PASS to provide it directly). Push a test image:
docker pull hello-world
docker tag hello-world registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app/hello-world:latest
docker push registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app/hello-world:latest
You should see the image layers pushed:
The push refers to repository [registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app/hello-world]
ac28800ec8bb: Pushed
latest: digest: sha256:... size: 524
Pull the image back to verify:
docker pull registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app/hello-world:latest
You should see:
latest: Pulling from hello-world
Digest: sha256:...
Status: Image is up to date for registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app/hello-world:latest

Enable the web UI (optional)

Zot ships with an optional web UI that lets you browse repositories, tags, and image manifests in a browser. The UI is disabled by default. To turn it on, enable the ui and search Zot extensions in the Zot config. Both are required: the UI depends on the search extension for catalog data. Add the following extensions block to the http object in zot-values.yaml. Place it as a sibling of log:
zot-values.yaml: configFiles.config.json additions
"log": { "level": "info" },
"extensions": {
  "ui":     { "enable": true },
  "search": { "enable": true }
}
Apply the change with helm upgrade:
helm upgrade zot zotregistry/zot \
  --version 0.1.98 \
  --atomic \
  --timeout 120s \
  -f zot-values.yaml
Wait for the pod to roll out:
kubectl rollout status deployment/zot --timeout=60s
Open the UI in a browser:
https://registry.[ORG-ID]-[CLUSTER-NAME].coreweave.app/
The UI prompts for credentials. Sign in with the admin username and password from the Create registry credentials step.
Retrieving the login passwordZot stores only the bcrypt hash of the password in the registry-htpasswd Secret, so the plaintext password cannot be read back from the cluster. If your shell still has the $REGISTRY_PASS environment variable from the Create registry credentials step, print it with:
echo "$REGISTRY_PASS"
If $REGISTRY_PASS is no longer set, use the plaintext password you chose when generating the htpasswd entry. If you’ve lost the password, rotate it by generating a new htpasswd entry and applying it through helm upgrade:
export REGISTRY_USER="your-username"
export REGISTRY_PASS="your-new-password"
export HTPASSWD=$(htpasswd -nbB "$REGISTRY_USER" "$REGISTRY_PASS")
Update the secretFiles.htpasswd field in zot-values.yaml with the new $HTPASSWD value, then:
helm upgrade zot zotregistry/zot \
  --version 0.1.98 \
  --atomic \
  --timeout 120s \
  -f zot-values.yaml
Sign in with the new credentials.

What you’ve deployed

You now have a Zot container registry running on CKS with htpasswd authentication, S3-backed storage through LOTA, a Let’s Encrypt TLS certificate issued via a DNS01 challenge, and a public FQDN under the .coreweave.app domain. External clients connect directly to the Zot LoadBalancer Service on port 443. Zot terminates TLS inside the pod using the mounted registry-cert Secret, and no ingress controller sits in the request path. cert-manager renews the certificate automatically before expiry. To pull images from this registry in your CKS workloads, create an imagePullSecret with the same credentials used in the Create registry credentials step.

Clean up

To remove the registry and all resources installed in this tutorial:
helm uninstall zot
kubectl delete secret registry-htpasswd
kubectl delete certificate registry-cert
kubectl delete secret registry-cert
helm uninstall cert-manager -n cert-manager
Uninstalling cert-manager leaves its Custom Resource Definitions (CRDs) in the cluster. To remove them completely:
kubectl get crd -o name | grep cert-manager | xargs kubectl delete

Additional resources

Last modified on April 22, 2026