Deliver images to a private registry
Select a controller, runtime, and Helm chart from a verified source commit, then make the images available to every eligible Kubernetes node. This guide copies an already published image pair from public GHCR to private Amazon Elastic Container Registry (ECR) without rebuilding it. Use the production installation guide to configure and install the resulting references. Operators can instead build their own images from the selected clean source; those builds have their own digests and need independent verification.
Select a release and chart
From a successful container publication, record its source commit, successful CI run, publication run and attempt, and both immutable image digests. Check the publication receipt and remote digests; a tag alone does not establish the image contents. Publication smoke checks do not prove a production deployment. Select images that include the features you need and support the target node architectures.
For the current production example, select a controller supporting the curated
Codex catalog (catalogSource: openai-curated) and the
browser request origin check.
Use a clean checkout at that exact commit for the Helm chart, Installation
examples, and helpers. From the repository root, compare git rev-parse HEAD
with the recorded full source SHA and check git status --short for changes.
If a separately published chart is available, verify its publication receipt,
source commit, chart digest, and image annotations against the selected image publication;
use it only when the release owner confirms compatibility. Chart publication
is a separate process and is not assumed to have occurred for an image release.
Copy the verified images to ECR
The operator needs Skopeo, AWS CLI, ECR push and read access, existing ECR repositories, and network access to both registries. GHCR source pulls are public. Verify that both ECR repositories enforce immutable tags, with no exclusions for the chosen tags, and restrict other writers. Choose a unique tag for this publication; never replace an existing tag. See ECR tag immutability. The ECR account and Region must be the ones approved for the cluster.
Set the values from the publication record and the target registry. The source references must be the published index digests, not architecture-specific child manifests. Replace all placeholders:
export AWS_REGION='<aws-region>'export ECR_REGISTRY='<aws-account-id>.dkr.ecr.<aws-region>.amazonaws.com'export SOURCE_CONTROLLER='ghcr.io/openclaw/openclaw-enterprise-controller@sha256:<64-hex-digest>'export SOURCE_RUNTIME='ghcr.io/openclaw/openclaw-enterprise-runtime@sha256:<64-hex-digest>'export RUNTIME_SOURCE="docker://$SOURCE_RUNTIME"export ECR_CONTROLLER="$ECR_REGISTRY/openclaw-enterprise/controller"export ECR_RUNTIME="$ECR_REGISTRY/openclaw-enterprise/runtime"export ECR_TAG='<unique-release-tag>'Start a dedicated Bash shell for registry authentication and copying. Its cleanup trap removes the credential file when the shell exits or is interrupted; do not run this in a shell with other work or traps:
bashIn that shell, create the private auth file, register cleanup immediately, and
authenticate to ECR. If an operation fails unexpectedly, exit this shell before
investigating or retrying; start a fresh one and authenticate again. The tag
lookup below can return the expected ImageNotFoundException.
{ set -o pipefail umask 077 REGISTRY_AUTH="$(mktemp "${TMPDIR:-/tmp}/oce-registry-auth.XXXXXXXX")" || exit 1 export REGISTRY_AUTH trap 'rm -f -- "$REGISTRY_AUTH"' EXIT trap 'exit 130' INT trap 'exit 143' TERM printf '{"auths":{}}\n' > "$REGISTRY_AUTH" || exit 1 aws ecr get-login-password --region "$AWS_REGION" | \ skopeo login --authfile "$REGISTRY_AUTH" --username AWS --password-stdin "$ECR_REGISTRY"}Run aws sts get-caller-identity and confirm the selected account before writing
to ECR. The temporary auth file contains registry credentials. The ECR login
expires; renew it before retrying if necessary. Before copying, inspect each
target tag using aws ecr describe-images in the approved account and Region. An
ImageNotFoundException establishes that the tag is absent; permission,
network, and other errors do not. For each repository, read its policy and the
exact target tag, substituting the repository name:
aws ecr describe-repositories --region "$AWS_REGION" \ --repository-names openclaw-enterprise/controller openclaw-enterprise/runtime \ --query 'repositories[].{name:repositoryName,mutability:imageTagMutability,exclusions:imageTagMutabilityExclusionFilters}'aws ecr describe-images --region "$AWS_REGION" \ --repository-name openclaw-enterprise/controller --image-ids "imageTag=$ECR_TAG" \ --query 'imageDetails[0].imageDigest' --output textRepeat the tag lookup for openclaw-enterprise/runtime. If a tag exists,
compare its digest with the source and use it only if it matches. Stop on a
conflicting digest. The repository's immutability policy protects against
overwrites but does not replace this readback or coordination with other writers.
Use a qualified local runtime
If you built and qualified the runtime locally, you can use its OCI archive as the source for the runtime copy. Use the root index digest recorded during qualification. An index can include a platform image and a provenance attestation even when the runtime supports only one architecture. A platform-specific export can omit the attestation and change the digest.
In the dedicated Bash shell above, select a new archive path in your private build directory. Export the complete image and compare the archive's raw index with the qualification record before copying it to ECR.
RUNTIME_INDEX='sha256:<qualified-runtime-index-digest>'RUNTIME_ARCHIVE='<absolute-path-to-new-runtime.tar>'( set -o noclobber; docker image save "$RUNTIME_INDEX" > "$RUNTIME_ARCHIVE" ) || exit 1RUNTIME_SOURCE="oci-archive:$RUNTIME_ARCHIVE"archive_hash=$(skopeo inspect --raw "$RUNTIME_SOURCE" | sha256sum) || exit 1[[ "sha256:${archive_hash%% *}" == "$RUNTIME_INDEX" ]] || exit 1If the digest differs, recover the complete index and its referenced manifests from the build's image store. Use the same qualified index digest for the destination readback.
Copy and verify the destination
Copy each absent tag and verify the destination digest before continuing. Run each command separately and stop on an error:
skopeo copy --all --preserve-digests --authfile "$REGISTRY_AUTH" \ "docker://$SOURCE_CONTROLLER" "docker://$ECR_CONTROLLER:$ECR_TAG"skopeo copy --all --preserve-digests --authfile "$REGISTRY_AUTH" \ "$RUNTIME_SOURCE" "docker://$ECR_RUNTIME:$ECR_TAG" skopeo inspect --raw --authfile "$REGISTRY_AUTH" \ "docker://$ECR_CONTROLLER:$ECR_TAG" | sha256sumskopeo inspect --raw --authfile "$REGISTRY_AUTH" \ "docker://$ECR_RUNTIME:$ECR_TAG" | sha256sumCompare each printed hash, prefixed with sha256:, to the corresponding
index digest in the receipt. Hashing the raw manifest checks the index itself,
rather than selecting a platform-specific manifest. --all copies the
platform manifests, and --preserve-digests fails if the registry cannot preserve them. If a copy times out or fails, read
back that exact target tag before retrying: accept a matching digest, stop on
a different digest, and retry only an absent tag after resolving the error.
If readback itself fails, the outcome is unknown. Inspect both destinations,
since one copy can succeed while the other fails.
Record the source and destination references with the chart and receipt. Exit the dedicated shell; its trap removes the auth file. In the original shell, export the verified readback digests:
exitexport CONTROLLER_IMAGE="$ECR_CONTROLLER@sha256:<verified-controller-digest>"export RUNTIME_IMAGE="$ECR_RUNTIME@sha256:<verified-runtime-digest>"Configure node pull access
Configure node pull permissions separately. OCE adds no imagePullSecrets to the Pods
it creates, so every node that runs control-plane or Agent Pods needs its own pull
access. Don't pull an OCE image through a Pod's imagePullSecrets either: where
kubelet verifies pull credentials (KubeletEnsureSecretPulledImages), every later Pod
on that node that uses the image without that Secret, such as a restarted API or
worker or the initialization Job, must then pull it again, which fails without node
access.
EKS managed nodes use their node IAM role for ECR pulls; cross-account repository policies may also be needed. For nodes without internet egress, configure ECR API and registry endpoints, S3 layer access, DNS, routes, security groups, and endpoint policies as described in ECR VPC endpoints and ECR images on EKS. Builder credentials do not grant node access. Confirm actual pulls on both control-plane and Agent nodes for the intended architectures. Direct public GHCR pulls need internet egress to GHCR and its layer hosts, not pull credentials.
Continue at Configure the Installation with the two ECR digest references. Use the same runtime digest for gateway and Agent unless a separately verified pair is required.
