OCI Registry Mirroring With zot¶
A
zotregistry can mirror one or more upstream OCI registries, including popular cloud registries such as Docker Hub and Google Container Registry (gcr.io).
A key use case for zot is to act as a mirror for upstream registries. If an upstream registry is OCI distribution-spec conformant for pulling images, you can use zot's sync feature to implement a downstream mirror, synchronizing OCI images and corresponding artifacts. Because synchronized images are stored in zot's local storage, registry mirroring allows for a fully distributed disconnected container image build pipeline. Container image operations terminate in local zot storage, which may reduce network latency and costs.
Beginning with zot v2.1.22, sync preserves upstream media types and digests. To sync Docker-format images (
application/vnd.docker.distribution.manifest.v2+json), enablehttp.compatas["docker2s2"]. Without this compatibility setting, zot rejects Docker schema 2 content instead of converting it to OCI.
Mirroring modes¶
For mirroring an upstream registry, two common use cases are a fully mirrored or a pull through (on-demand) cache registry.
As with git, wherein every clone is a full repository, you can configure your local zot instance to be a fully mirrored OCI registry. For this mode, configure zot for synchronization by periodic polling, not on-demand. Zot copies and caches a full copy of every image on the upstream registry, updating the cache whenever polling discovers a change in content or image version at the upstream registry.
For a pull through cache mirrored registry, configure zot for on-demand synchronization. When an image is first requested from the local zot registry, the image is downloaded from the upstream registry and cached in local storage. Subsequent requests for the same image are served from zot's cache. Images that have not been requested are not downloaded. If a polling interval is also configured, zot periodically polls the upstream registry for changes, updating any cached images if changes are detected.
Because Docker Hub rate-limits pulls and does not support catalog listing, do not use polled mirroring with Docker Hub. Use only on-demand mirroring with Docker Hub.
When to enable compat¶
Enable http.compat: ["docker2s2"] when any of the following apply:
- Upstream images use Docker Image Manifest v2, Schema 2 and you need to store them unchanged (common on Docker Hub and operator/catalog images)
- Docker-format upstream images have cosign or notation signatures and referrers tied to their Docker manifest digests
You can omit compat only when all synchronized content is OCI-formatted. Sync preserves the upstream digest in either case.
| Workload | http.compat |
|---|---|
| OCI-only upstream | omit |
| Mixed Docker/OCI upstream | ["docker2s2"] |
| On-demand pull-through cache for all image types | ["docker2s2"] |
![]()
preserveDigestis deprecated and ignored beginning with v2.1.22. Remove it from new configurations. Digest preservation is automatic, whilehttp.compatcontrols whether Docker schema 2 content is accepted.
Migrating or updating a registry using mirroring¶
Mirroring zot using the sync feature allows you to easily migrate a registry. In situations such as the following, zot mirroring provides an easy solution.
-
Migrating an existing zot or non-zot registry to a new location.
Provided that the source registry is OCI-compliant for image pulls, you can mirror the registry to a new zot registry, delete the old registry, and reroute network traffic to the new registry.
-
Updating (or downgrading) a zot registry.
To minimize downtime during an update, or to avoid any incompatibilities between zot releases that would preclude an in-place update, you can bring up a new zot registry with the desired release and then migrate from the existing registry.
To ensure a complete migration of the registry contents, set a polling interval in the configuration of the new zot registry and set prefix to **, as shown in this example:
{
"urls": [
"https://registry1:5000"
],
"pollInterval": "12h",
"onDemand": true,
"content": [
{
"prefix": "**"
}
]
}
Basic configuration for mirroring with sync¶
The sync feature of zot is an extension of the OCI-compliant registry implementation. You can configure the sync feature under the extensions section of the zot configuration file, as shown in this example.
Configure http.compat: ["docker2s2"] in the same configuration file when an upstream can return Docker schema 2 content. See When to enable compat.
"extensions": {
"sync": {
"credentialsFile": "./examples/sync-auth-filepath.json",
"registries": [
{
"urls": [
"https://registry1:5000"
],
"onDemand": false,
"pollInterval": "6h",
"platforms": ["linux/amd64", "linux/arm64"],
"tlsVerify": true,
"certDir": "/home/user/certs",
"maxRetries": 3,
"retryDelay": "5m",
"maxRetryDelay": "30m",
"onlySigned": true,
"content": [
{
"prefix": "/repo2/repo",
"tags": {
"regex": "4.*",
"semver": true
},
"destination": "/repo2",
"stripPrefix": true,
"platforms": ["linux/amd64"]
}
]
}
]
}
}
The following table lists the configurable attributes for the sync feature:
| Attribute | Description |
|---|---|
credentialsFile | The location of a local file containing credentials for other registries, as in the following example: { |
urls | A list of one or more URLs to an upstream image registry. If the main URL fails, the sync process will try the next URLs in the listed order. |
onDemand |
|
manifestCheckInterval | For on-demand sync, the minimum interval between upstream manifest checks for a tag that is already cached locally. During this interval, zot serves the cached manifest without contacting the upstream registry. The default is |
onDemandInBackground | When |
pollInterval | The period in seconds between polling of a remote registry. If no value is specified, no periodic polling will occur. If a value is set and the content attributes are configured, periodic synchronization is enabled and will run at the specified value. |
platforms | Optional periodic-sync allowlist of platform strings in |
tlsVerify |
|
certDir | If a path is specified, use certificates (*.crt, *.cert, *.key files) at this path when connecting to the destination registry or daemon. If no path is specified, use the default certificates directory. |
maxRetries | The maximum number of retries if an error occurs during either an on-demand or periodic synchronization. If no value is specified, no retries will occur. |
retryDelay | The interval in seconds between retries. This attribute is mandatory when maxRetries is configured. |
maxRetryDelay | Optional maximum HTTP retry delay. When greater than retryDelay, retry delays use exponential backoff up to this value. It requires retryDelay and cannot be smaller than it. When omitted, it defaults to retryDelay, preserving a fixed retry interval. |
syncTimeout | The timeout duration for on-demand sync operations. This timeout applies to the entire image sync operation, including downloading the manifest and all associated blobs (layers, config, referrers, etc.). If the requesting client disconnects, the sync operation will continue in the background until this timeout is reached. If not specified or set to zero, the default timeout of 3 hours is used. This prevents sync operations from being cancelled when HTTP clients disconnect (e.g., Kubernetes timeout/retries). |
onlySigned |
|
preserveDigest | Deprecated and ignored beginning with v2.1.22. Sync always preserves upstream digests. Enable |
syncLegacyCosignTags | When |
reqConcurrent | Maximum number of concurrent requests to each upstream host. The default is |
reqPerSec | Maximum request rate to each upstream host. Requests are unlimited by default. A configured value must be finite and greater than zero. |
disableHTTP2 | Set to |
maxIdleConnsPerHost | Maximum number of idle HTTP connections retained for each upstream host. A configured value must be greater than zero. When unset, the Go HTTP transport default is |
content | The included attributes in this section specify which content will be pulled. If this section is not populated, periodic polling will not occur. The included attributes can also filter which on-demand images are pulled. |
prefix | On the remote registry, the path from which images will be pulled. This path can be a string that exactly matches the remote path, or it can be a glob pattern. For example, the path can include a wildcard (*) or a recursive wildcard (**). |
platforms | Optional periodic-sync platform allowlist for this content rule, using the same entry forms as the registry-level platforms value. When present, it overrides that value. An empty list selects all platforms; omission inherits the registry-level value. On-demand sync ignores this setting. |
tags | The included attributes in this optional section specify how remote images will be selected for synchronization based on image tags. |
tags.regex | Specifies a regular expression for matching image tags. Images whose tags do not match the expression are not pulled. |
tags.semver | Specifies whether image tags are to be filtered by semantic versioning (semver) compliance.
|
destination | Specifies the local path in which pulled images are to be stored. |
stripPrefix | Specifies whether the prefix path from the remote registry will be retained or replaced when the image is stored in the zot registry.
|
Selecting platforms for periodic sync¶
Use platforms to create a sparse local copy of a multi-architecture image. The registry-level list supplies the default for periodic sync, and a content rule can override it:
{
"urls": ["https://registry.example.com"],
"pollInterval": "6h",
"platforms": ["linux/amd64", "linux/arm64"],
"content": [{
"prefix": "edge/**",
"platforms": ["linux/arm64/v8"]
}]
}
The index retains its upstream digest even when only selected child manifests are stored. Storage validation, garbage collection, scanning, and scrub tolerate children that were intentionally omitted. On-demand sync remains demand-driven and ignores both platform lists.
Platform entries can use os/arch, os/arch/variant, or a bare architecture such as amd64. Include an empty string ("") in a nonempty allowlist to retain descriptors that do not declare a platform, including attestation-style entries.
On-demand sync in the background¶
Set both onDemand and onDemandInBackground to return immediately when a manifest is absent locally and populate it asynchronously:
The initial manifest request receives a real 404; later requests succeed after the background sync completes. Blob misses alone do not start background sync. Do not enable this mode when zot is the client's only registry, and do not combine it with manifestCheckInterval.
Controlling on-demand upstream requests¶
The following example revalidates a cached tag at most once every five minutes and limits each upstream host to 20 concurrent requests and 100 requests per second. It also uses a larger HTTP/1.1 connection pool for high-throughput transfers.
{
"extensions": {
"sync": {
"registries": [
{
"urls": ["https://registry.example.com"],
"onDemand": true,
"manifestCheckInterval": "5m",
"reqConcurrent": 20,
"reqPerSec": 100,
"disableHTTP2": true,
"maxIdleConnsPerHost": 20
}
]
}
}
}
These limits are applied independently for each upstream host. When concurrent clients request the same uncached image, zot coalesces those requests into one sync operation.
Configuring mirroring modes¶
Two mirroring modes were described in this document: - periodic - the registry syncs images matching specific patters from the upstream registries at a given polling interval - on demand - the registry reaches out to the upstream registries when the image is requested by the user
These two modes can be configured, separately or together, using specific settings. See the table below for details:
onDemand | pollInterval | content | Result |
|---|---|---|---|
| false | omitted | omitted | sync is disabled |
| false | >0 | omitted | sync is disabled |
| false | omitted | at least 1 entry | sync is disabled |
| false | >0 | at least 1 entry | sync is enabled in periodic mode for the images matching the content patterns |
| true | omitted | omitted | sync is enabled in on demand mode for any image |
| true | >0 | omitted | sync is enabled in on demand mode for any image |
| true | omitted | at least 1 entry | sync is enabled in on demand mode for the images matching the content patterns |
| true | >0 | at least 1 entry | sync is enabled in both periodic and on demand modes for the images matching the content patterns |
Configuration examples for mirroring¶
Example: Multiple repositories with polled mirroring¶
The following is an example of sync configuration for mirroring multiple repositories with polled mirroring. If the upstream can return Docker schema 2 content, the complete configuration file must also include http.compat: ["docker2s2"] (see When to enable compat).
"sync": {
"enable": true,
"credentialsFile": "./examples/sync-auth-filepath.json",
"registries": [
{
"urls": [
"https://registry1:5000"
],
"onDemand": false,
"pollInterval": "6h",
"tlsVerify": true,
"certDir": "/home/user/certs",
"maxRetries": 3,
"retryDelay": "5m",
"onlySigned": true,
"content": [
{
"prefix": "/repo1/repo",
"tags": {
"regex": "4.*",
"semver": true,
"tags": {
"excludeRegex": ".*-(amd64|arm64)$"
}
}
},
{
"prefix": "/repo2/repo",
"destination": "/repo2",
"stripPrefix": true
},
{
"prefix": "/repo3/repo"
}
]
}
]
}
The configuration in this example will result in the following behavior:
- Only signed images (notation and cosign) are synchronized.
- The sync communication is secured using certificates in
certDir. - This registry synchronizes with upstream registry every 6 hours.
- Upstream digests are preserved. Enable
http.compat: ["docker2s2"]in the full configuration file when the upstream can return Docker schema 2 content. - On-demand mirroring is disabled.
- Based on the content filtering options, this registry synchronizes these images:
- From /repo1/repo, images with tags that begin with "4." and are semver compliant but excluding some tag patterns
Files are stored locally in /repo1/repo on localhost. - From /repo2/repo, images with all tags.
BecausestripPrefixis enabled, files are stored locally in /repo2. For example, docker://upstream/repo2/repo:v1 is stored as docker://local/repo2:v1. - From /repo3/repo, images with all tags.
Files are stored locally in /repo3/repo.
- From /repo1/repo, images with tags that begin with "4." and are semver compliant but excluding some tag patterns
Example: Multiple registries with on-demand mirroring¶
The following is an example of sync configuration for mirroring multiple registries with on-demand mirroring.
{
"distSpecVersion": "1.0.1",
"storage": {
"rootDirectory": "/tmp/zot",
"gc": true
},
"http": {
"address": "0.0.0.0",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"sync": {
"enable": true,
"registries": [
{
"urls": ["https://k8s.gcr.io"],
"content": [
{
"prefix": "**",
"destination": "/k8s-images"
}
],
"onDemand": true,
"tlsVerify": true,
"syncTimeout": "10m"
},
{
"urls": [
"https://index.docker.io"
],
"content": [
{
"prefix": "**",
"destination": "/docker-images"
}
],
"onDemand": true,
"tlsVerify": true
}
]
}
}
}
With this zot configuration, the sync behavior is as follows:
-
This initial user request for content from the zot registry:
skopeo copy --src-tls-verify=false docker://localhost:8080/docker-images/alpine <dest>
causes zot to synchronize the content with the docker.io registry:
docker.io/library/alpine:latest
to the zot registry:
localhost:8080/docker-images/alpine:latest
before delivering the content to the requestor at<dest>. -
This initial user request for content from the zot registry:
skopeo copy --src-tls-verify=false docker://localhost:8080/k8s-images/kube-proxy:v1.19.2 <dest>
causes zot to synchronize the content with the gcr.io registry:
k8s.gcr.io/kube-proxy:v1.19.2
to the zot registry:
localhost:8080/k8s-images/kube-proxy:v1.19.2
before delivering the content to the requestor at<dest>.
You can use this command:
curl http://localhost:8080/v2/_catalog
to display the local repositories:
Example: Multiple registries with mixed mirroring modes¶
The following is an example of a zot configuration file for mirroring multiple upstream registries.
{
"distSpecVersion": "1.1.0-dev",
"storage": {
"rootDirectory": "/tmp/zot"
},
"http": {
"address": "127.0.0.1",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"sync": {
"enable": true,
"credentialsFile": "./examples/sync-auth-filepath.json",
"registries": [
{
"urls": [
"https://registry1:5000"
],
"onDemand": false,
"pollInterval": "6h",
"tlsVerify": true,
"certDir": "/home/user/certs",
"maxRetries": 3,
"retryDelay": "5m",
"onlySigned": true,
"content": [
{
"prefix": "/repo1/repo",
"tags": {
"regex": "4.*",
"semver": true
}
},
{
"prefix": "/repo1/repo",
"destination": "/repo",
"stripPrefix": true
},
{
"prefix": "/repo2/repo"
}
]
},
{
"urls": [
"https://registry2:5000",
"https://registry3:5000"
],
"pollInterval": "12h",
"tlsVerify": false,
"onDemand": false,
"content": [
{
"prefix": "/repo2",
"tags": {
"semver": true
}
}
]
},
{
"urls": [
"https://index.docker.io"
],
"onDemand": true,
"tlsVerify": true,
"maxRetries": 6,
"retryDelay": "5m"
}
]
}
}
}
Example: Support for subpaths in local storage¶
{
"distSpecVersion": "1.0.1",
"storage": {
"subPaths":{
"/kube-proxy":{
"rootDirectory": "/tmp/kube-proxy",
"dedupe": true,
"gc": true
}
},
"rootDirectory": "/tmp/zot",
"gc": true
},
"http": {
"address": "0.0.0.0",
"port": "8080"
},
"log": {
"level": "debug"
},
"extensions": {
"sync": {
"enable": true,
"registries": [
{
"urls": ["https://k8s.gcr.io"],
"content": [
{
"destination": "/kube-proxy",
"prefix": "**"
}
],
"onDemand": true,
"tlsVerify": true,
"maxRetries": 2,
"retryDelay": "5m"
}
]
}
}
}
- This user request for content from the zot registry:
skopeo copy --src-tls-verify=false docker://localhost:8080/kube-proxy/kube-proxy:v1.19.2 <dest>
causes zot to synchronize the content with this remote registry:
k8s.gcr.io/kube-proxy:v1.19.2
to the zot registry:
localhost:8080/kube-proxy/kube-proxy:v1.19.2
before delivering the content to the requestor at<dest>.
You can use this command:
curl http://localhost:8080/v2/_catalog
to display the local repositories:
In zot storage, the requested content is located here:
/tmp/zot/kube-proxy/kube-proxy/kube-proxy/
This subpath is created from the following path components:
/tmp/zotis therootDirectoryof the zot registrykube-proxyis therootDirectoryof the storage subpathkube-proxyis the syncdestinationparameterkube-proxyis the repository name
Example: Support for AWS ECR¶
This is an example configuration demonstrating how to use the sync extension with Amazon ECR (Elastic Container Registry) credential helper. The configuration enables zot to synchronize container images from an ECR registry.
"extensions": {
"sync": {
"credentialsFile": "",
"DownloadDir": "/tmp/zot",
"registries": [
{
"urls": [
"https://ACCOUNTID.dkr.ecr.REGION.amazonaws.com"
],
"onDemand": true,
"maxRetries": 5,
"retryDelay": "2m",
"credentialHelper": "ecr"
}
]
}
}
Example: Google registry authentication¶
Use the gcp credential helper to obtain a read-only access token from Google Application Default Credentials. It supports GCE/GKE metadata credentials, GOOGLE_APPLICATION_CREDENTIALS, workload identity federation external-account files, and gcloud user credentials. The principal still needs permission to read the upstream repository, such as roles/artifactregistry.reader.
"extensions": {
"sync": {
"registries": [{
"urls": ["https://REGION-docker.pkg.dev"],
"onDemand": true,
"credentialHelper": "gcp"
}]
}
}
The helper also works with Google Container Registry hostnames that accept Google access tokens. It requests the cloud-platform.read-only scope and refreshes the short-lived token through the configured ADC token source.
Example: OAuth2 credential helper¶
The oauth2 helper exchanges an assertion for a short-lived registry access token and refreshes it before expiry. Configure exactly one assertion source: assertionFile for an externally issued token that can rotate on disk, or signingFile for a JSON configuration that lets zot mint a fresh single-use JWT for each exchange.
This RFC 8693 example exchanges a rotating workload identity token with a security token service:
"extensions": {
"sync": {
"registries": [{
"urls": ["https://registry.example.com"],
"onDemand": true,
"credentialHelper": "oauth2",
"oauth2CredentialHelper": {
"tokenURL": "https://sts.example.com/v1/token",
"assertionFile": "/run/secrets/subject-token",
"grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
"audience": "//sts.example.com/pools/example/providers/workload",
"subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
"requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
"username": "oauth2accesstoken"
}
}]
}
}
The oauth2CredentialHelper attributes are:
| Attribute | Description |
|---|---|
tokenURL | Required OAuth2 token endpoint. |
assertionFile | Path to a pre-signed assertion. Required unless signingFile is set and mutually exclusive with it. The file is re-read on every refresh. |
signingFile | Path to a JSON signing configuration used to mint assertions. Required unless assertionFile is set and mutually exclusive with it. |
grantType | Grant type. Defaults to client_credentials; also supports urn:ietf:params:oauth:grant-type:jwt-bearer and urn:ietf:params:oauth:grant-type:token-exchange. |
clientID | Optional OAuth2 client identifier. |
clientSecretFile | Optional file containing the OAuth2 client secret. |
scopes | Optional list of OAuth2 scopes, sent as a space-separated scope value. |
username | Registry username paired with the access token. Defaults to <token>. |
audience | Required for RFC 8693 token exchange and rejected for other grant types. |
subjectTokenType | RFC 8693 subject token type URI. Defaults to urn:ietf:params:oauth:token-type:jwt. |
requestedTokenType | RFC 8693 requested token type URI. Defaults to urn:ietf:params:oauth:token-type:access_token. |
A signingFile contains privateKeyFile and algorithm, plus optional keyId, issuer, subject, and audience values. The issuer and subject default to clientID; the audience defaults to tokenURL.