Install the production control plane
Prepare Kubernetes or EKS, the production prerequisites, and workspace routing, including a GatewayClass. Keep private routing enabled. Start with the password profile and native Agent administration; optional GitHub or Google sign-in requires disabling native administration.
Configure with an installation profile or, for advanced customization, manual YAML.
Use a clean checkout matching the image revision. Follow private-registry delivery for provenance, ECR copies, and chart selection. Retain this shell and protected files for Agent deployment.
Use published images
Use ghcr.io/openclaw/openclaw-enterprise-controller:latest and
ghcr.io/openclaw/openclaw-enterprise-runtime:latest through the
published-image procedure. It pulls the pair, checks
their source revisions, and sets CONTROLLER_IMAGE and RUNTIME_IMAGE for this
runbook. It also explains how to select the matching source checkout and chart.
Complete it before generating configuration; then skip the build section below.
Configure pull credentials for control-plane and tenant Pods. Workstation
docker login does not authenticate cluster nodes. For registry copies and
published charts, follow private-registry delivery.
Build and publish production images
Use the separately approved publication workflow or build these images for your registry:
| Image | Source | Used by |
|---|---|---|
| Controller | Root Dockerfile, target runtime |
API, worker, migration, and bootstrap |
| Runtime | deploy/runtime/Dockerfile, assembling pinned OpenClaw source and Codex |
Gateways and Agents (the same image serves both) |
With Buildx and registry push access, select your registry, repository, and node platform. The base image follows the runtime recipe.
Authenticate the builder with docker login <registry-host> and approved
credentials; for private ECR, follow ECR authentication.
Run this candidate-image push in a fresh Bash shell; stop on failure and retain metadata. The standard runtime packages Slack and Codex; use it for both slots unless you have separately verified a gateway/Codex pair. Installing packages at gateway startup is unsupported.
# Build from a clean checkout.export OCC_IMAGE_REGISTRY="${OCC_IMAGE_REGISTRY:-registry.example.com}"export OCC_IMAGE_REPOSITORY="${OCC_IMAGE_REPOSITORY:-$OCC_IMAGE_REGISTRY/your-team/openclaw-enterprise}"export OCC_IMAGE_PLATFORM="${OCC_IMAGE_PLATFORM:-linux/amd64}"export NODE_BASE_IMAGE='docker.io/library/node:24-bookworm@sha256:934240a162082fd8b8a2f90cd5114446443f1eba1c5378f6687167ca405e6584'if unset CONTROLLER_IMAGE RUNTIME_IMAGE OCC_IMAGE_METADATA && OCC_IMAGE_TAG="$(git rev-parse HEAD)" && OCC_IMAGE_METADATA="$(mktemp -d)" && export OCC_IMAGE_TAG && docker buildx build --push --platform "$OCC_IMAGE_PLATFORM" --target runtime \ --metadata-file "$OCC_IMAGE_METADATA/controller.json" \ --build-arg NODE_BASE_IMAGE="$NODE_BASE_IMAGE" \ --build-arg OCC_BUILD_REVISION="$OCC_IMAGE_TAG" \ --label "org.opencontainers.image.revision=$OCC_IMAGE_TAG" \ -t "$OCC_IMAGE_REPOSITORY/controller:$OCC_IMAGE_TAG" . && docker buildx build --push --platform "$OCC_IMAGE_PLATFORM" \ --metadata-file "$OCC_IMAGE_METADATA/runtime.json" \ --build-arg NODE_BASE_IMAGE="$NODE_BASE_IMAGE" \ --build-arg OCC_BUILD_REVISION="$OCC_IMAGE_TAG" \ -f deploy/runtime/Dockerfile \ -t "$OCC_IMAGE_REPOSITORY/runtime:$OCC_IMAGE_TAG" . && CONTROLLER_DIGEST="$(yq -p=json -e -r '."containerimage.digest" | select(test("^sha256:[a-f0-9]{64}$"))' "$OCC_IMAGE_METADATA/controller.json")" && RUNTIME_DIGEST="$(yq -p=json -e -r '."containerimage.digest" | select(test("^sha256:[a-f0-9]{64}$"))' "$OCC_IMAGE_METADATA/runtime.json")" && [[ "$CONTROLLER_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]] && [[ "$RUNTIME_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]; then export CONTROLLER_IMAGE="$OCC_IMAGE_REPOSITORY/controller@$CONTROLLER_DIGEST" export RUNTIME_IMAGE="$OCC_IMAGE_REPOSITORY/runtime@$RUNTIME_DIGEST"else printf 'Build or digest extraction failed; stop. Metadata: %s\n' "${OCC_IMAGE_METADATA:-unavailable}" >&2 falsefiBefore installation, check each digest on native hosts for every target architecture, without skips. Use these digests in YAML and configure pull credentials for control-plane and tenant Pods.
Configure the Installation
Set the production shell before Kubernetes commands. This runbook uses Helm
release oce in Namespace openclaw-system. Keep configuration and
bootstrap PVC YAML in the protected OCC_INPUT_DIRECTORY; Secret inputs stay
under /secure/occ.
To reuse profile output or verified YAML from
local operations, set
OCC_INPUT_DIRECTORY to it, skip both generation branches, and continue with the
shared checks.
umask 077export OCC_INPUT_DIRECTORY="${OCC_INPUT_DIRECTORY:-/secure/occ}"export KUBECONFIG_FILE="$OCC_INPUT_DIRECTORY/kubeconfig": "${CONTEXT:?Set the reviewed Kubernetes context from your cluster guide}"install -d -m 700 /secure/occ "$OCC_INPUT_DIRECTORY"chmod 600 "$KUBECONFIG_FILE"kubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" versionKubernetes older than 1.35 is unsupported; the API and worker emit
compute.preflight-warning.
Recommended: generate profile configuration
Choose openclaw or codex from the profile options.
Generation requires Node.js 24+ on the operator host. Manual YAML skips that
requirement for installation, but the later Agent transport-provisioning example
also uses Node. Use console transport provisioning if Node is unavailable.
Create $OCC_INPUT_DIRECTORY/profile-input.json from the schema in
Render installation profiles. Set
controlPlane.releaseName to oce, controlPlane.namespace to
openclaw-system, controlPlane.controllerImage to $CONTROLLER_IMAGE, and
runtime.image to $RUNTIME_IMAGE. Keep credentials and tokens out of the
input JSON. Keep it separate from values.yaml, installation.yaml, and
preflight.json, which each render clears.
export OCC_PROFILE="${OCC_PROFILE:-codex}"export OCC_PROFILE_INPUT="${OCC_PROFILE_INPUT:-$OCC_INPUT_DIRECTORY/profile-input.json}"( set -e : "${CONTROLLER_IMAGE:?Set the controller digest reference}" : "${RUNTIME_IMAGE:?Set the runtime digest reference}" test -s "$OCC_PROFILE_INPUT" yq -e '.controlPlane.releaseName == "oce" and .controlPlane.namespace == "openclaw-system"' \ "$OCC_PROFILE_INPUT" >/dev/null node scripts/render-installation-profile.mjs \ --profile "$OCC_PROFILE" \ --input "$OCC_PROFILE_INPUT" \ --out-dir "$OCC_INPUT_DIRECTORY" test -s "$OCC_INPUT_DIRECTORY/values.yaml" test -s "$OCC_INPUT_DIRECTORY/installation.yaml")If rendering fails, stop, fix the input, and rerender; never substitute example
or manual YAML. This keeps values.yaml, installation.yaml, and
controlPlane.installationChecksum paired.
Advanced: copy manual YAML examples
Use this branch only when deliberately skipping profiles; it refuses to overwrite
existing configuration YAML. The manual example selects the curated Codex PluginDriver catalog,
unlike the codex profile's default hosted PAT-backed discovery.
( set -e test ! -e "$OCC_INPUT_DIRECTORY/values.yaml" test ! -e "$OCC_INPUT_DIRECTORY/installation.yaml" install -m 600 deploy/examples/production/values.yaml "$OCC_INPUT_DIRECTORY/values.yaml" install -m 600 deploy/examples/production/installation.yaml "$OCC_INPUT_DIRECTORY/installation.yaml" : "${CONTROLLER_IMAGE:?Set the controller digest reference}" : "${RUNTIME_IMAGE:?Set the runtime digest reference}" yq -i '.images.controller = strenv(CONTROLLER_IMAGE)' "$OCC_INPUT_DIRECTORY/values.yaml" yq -i '.drivers.compute.configuration.images.gateway = strenv(RUNTIME_IMAGE) | .drivers.compute.configuration.images.agent = strenv(RUNTIME_IMAGE)' \ "$OCC_INPUT_DIRECTORY/installation.yaml")For registry-backed installs, compare selected images with the checked digests before continuing: profile installs check the input JSON image fields before rendering; manual installs check the edited YAML with Verify installation image selections.
Shared bootstrap PVC and configuration checks
Create the bootstrap PVC manifest if it is absent:
test -e "$OCC_INPUT_DIRECTORY/bootstrap-pvc.yaml" || \ install -m 600 deploy/examples/production/bootstrap-pvc.yaml "$OCC_INPUT_DIRECTORY/bootstrap-pvc.yaml"chmod 600 "$OCC_INPUT_DIRECTORY/values.yaml" \ "$OCC_INPUT_DIRECTORY/installation.yaml" \ "$OCC_INPUT_DIRECTORY/bootstrap-pvc.yaml"For profile installs, change $OCC_PROFILE_INPUT and rerender instead of editing
values.yaml or installation.yaml. For manual installs, edit the copied YAML
before running the checks:
values.yaml: set auth URL, admin email, database and cluster CIDRs, control-plane node selector, database CA, DNS, API clients, and bootstrap password claim. Keep native admin enabled for the password profile, and gateway routing enabled with the reviewed GatewayClass and Secret names.installation.yaml: set cluster name, log level, DNS selectors, service-principal token settings, Secret prefixes, runtime storage class, immutable runtime image digests, and PluginDriver catalog. Setruntime.gatewayNodeSelectorandruntime.nodeSelectorto disjoint Ready pools; Helm does not place runtimes. Omitnetwork.gatewayClientswith routing enabled.presets.includeDefaults: falsedisables the bundled Presets.
For every install, set bootstrap-pvc.yaml name, namespace, size, and
protected storageClassName.
For logging.level, see Choose the log level.
Configure native admin domains through native admin setup.
For Slack Agents, configure both proxy paths in the
Slack guide. For Codex
sandboxing, follow Codex sandbox setup.
Run every check below before provisioning the password profile:
yq e -e '.images.controller | test("@sha256:[a-f0-9]{64}$")' \ "$OCC_INPUT_DIRECTORY/values.yaml" >/dev/nullyq e -e '.auth.baseUrl != "" and .bootstrap.adminEmail != "" and (.database.cidrs | length > 0) and (.cluster.cidrs | length > 0) and (.controlPlane.nodeSelector | length > 0) and (.api.clients | length > 0) and .gatewayRouting.enabled == true and .gatewayRouting.gatewayClassName != "" and .gatewayRouting.apiKeySecretName != "" and (.agentNativeAdmin.enabled == true or .auth.github.enabled == true or .auth.google.enabled == true)' \ "$OCC_INPUT_DIRECTORY/values.yaml" >/dev/nullyq e -e '.drivers.compute.configuration.images.requireImmutableDigest == true and (.drivers.compute.configuration.images.gateway | test("@sha256:[a-f0-9]{64}$")) and (.drivers.compute.configuration.images.agent | test("@sha256:[a-f0-9]{64}$")) and .drivers.compute.configuration.runtime.gatewayStorageClassName != "" and (.drivers.compute.configuration.runtime.gatewayNodeSelector | length > 0) and (.drivers.compute.configuration.runtime.nodeSelector | length > 0)' \ "$OCC_INPUT_DIRECTORY/installation.yaml" >/dev/nullyq e -e '.metadata.namespace == "openclaw-system" and .spec.storageClassName != ""' \ "$OCC_INPUT_DIRECTORY/bootstrap-pvc.yaml" >/dev/nullhelm template oce deploy/helm/openclaw-enterprise \ --namespace openclaw-system -f "$OCC_INPUT_DIRECTORY/values.yaml" \ >/tmp/oce-rendered.yamlexport CONTROLLER_IMAGE="$(yq e -r '.images.controller' "$OCC_INPUT_DIRECTORY/values.yaml")"export BOOTSTRAP_CLAIM="$(yq e -r '.bootstrap.password.claimName' "$OCC_INPUT_DIRECTORY/values.yaml")"test "$BOOTSTRAP_CLAIM" = "$(yq e -r '.metadata.name' "$OCC_INPUT_DIRECTORY/bootstrap-pvc.yaml")"$KUBECONFIG_FILE must select the same cluster as $CONTEXT.
Prepare these Secret inputs under /secure/occ, each containing one raw value
without quotes or a variable assignment.
| File | Contents and source |
|---|---|
occ-application-url |
PostgreSQL connection URL for the limited application role, used by bootstrap, the API, and the worker. Example shape: postgresql://occ_app:<url-encoded-password>@<postgres-host>:5432/<database>. |
occ-migration-url |
URL for a separate schema-migration role on the same database. Example shape: postgresql://occ_migrator:<url-encoded-password>@<postgres-host>:5432/<database>. Obtain this credential separately; do not give it to the API or worker. |
occ-database-ca.pem |
Optional PostgreSQL root CA bundle when the database root is not in the base image trust store. Required only when database.caSecretName is set. |
occ-auth-secret |
Random session-signing secret. Generate it once with the command below and retain it across redeployments; it is separate from other credentials. |
Save both database URLs in protected files, replacing placeholders and preserving
required TLS options. For managed PostgreSQL roots supplied through
database.caSecretName, set sslmode=verify-full and sslrootcert to the
mounted CA file in both URLs. With the example mount settings, the path is
/etc/openclaw/database-ca/ca.pem; if you change them, use
<database.caMountPath>/<database.caKey>. Start query parameters with ? and
join further ones with &. Generate the auth secret for a
new Installation; this command refuses to overwrite an existing file:
( umask 077 set -C openssl rand -hex 32 > /secure/occ/occ-auth-secret)chmod 600 /secure/occ/occ-application-url /secure/occ/occ-migration-url \ /secure/occ/occ-auth-secrettest -s /secure/occ/occ-application-urltest -s /secure/occ/occ-migration-urlexport DATABASE_CA_SECRET="$(yq e -r '.database.caSecretName // ""' "$OCC_INPUT_DIRECTORY/values.yaml")"export DATABASE_CA_KEY="$(yq e -r '.database.caKey // "ca.pem"' "$OCC_INPUT_DIRECTORY/values.yaml")"if [ -n "$DATABASE_CA_SECRET" ]; then chmod 600 /secure/occ/occ-database-ca.pem test -s /secure/occ/occ-database-ca.pemfiKeep these values out of Helm values, Installation YAML, Configurations, shell history, and the repository.
Prepare workspace access
Create the controller namespace:
kubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" create namespace openclaw-systemComplete Configure private routing:
create occ-private-gateway-key and match the Helm and Installation routing
settings. Rerun validation and rendering above if inputs change. Configure each
Agent's authentication during Agent deployment.
Provision system Secrets and install
Create system Secrets from protected files:
kubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" \ apply --dry-run=server -f /tmp/oce-rendered.yamlkubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" \ apply --dry-run=server -f "$OCC_INPUT_DIRECTORY/bootstrap-pvc.yaml"kubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" -n openclaw-system \ create secret generic occ-installation-startup --from-file=installation.yaml="$OCC_INPUT_DIRECTORY/installation.yaml"kubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" -n openclaw-system \ create secret generic occ-database --from-file=application-url=/secure/occ/occ-application-url --from-file=migration-url=/secure/occ/occ-migration-urlif [ -n "$DATABASE_CA_SECRET" ]; then kubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" -n openclaw-system \ create secret generic "$DATABASE_CA_SECRET" \ --from-file="$DATABASE_CA_KEY=/secure/occ/occ-database-ca.pem"fikubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" -n openclaw-system \ create secret generic occ-auth --from-file=secret=/secure/occ/occ-auth-secretThese operator-owned Secrets are not synchronized automatically. The optional CA
Secret mounts read-only in migration, bootstrap, API, and worker containers at
database.caMountPath; PostgreSQL URLs select sslrootcert.
Optional repository credential service
Enable repository credentials only after preparing the
repository service inputs and
GitHub Backend selection:
the immutable service image, registry ConfigMap, private service configuration,
App key, internal-Service TLS Secret, and public CA Secret. Mount one registry
version into the API, worker, and
service. The Compute network peer must select the worker Pod on port 8443; the
Service serves HTTPS on 443.
The chart runs one Recreate worker Pod with a credential sidecar. Only the
sidecar mounts the App and TLS private inputs, and it has no Kubernetes API token;
only the worker container's token has tenant Secret access, and the worker stays
trusted per tenant namespace.
NetworkPolicies let managed gateways reach the service and the worker reach
approved provider CIDRs; registry and session checks enforce exact scope. Startup
rejects limits.shutdownGraceMs above 60000 to finish cleanup within the
Pod's 75-second grace. Restart the API and worker together after registry
or service input changes; readiness does not prove token minting or Agent Git
workflows.
Azure PostgreSQL workload identity
For Azure workload-identity database authentication,
use password-free URLs with verified TLS in the database URL files above.
Prepare identity environment variables and a renewed federation-token projection
for each connecting process: migration, bootstrap, API, and worker.
Provision federation and database grants for separate application and migrator
identities; keep the migrator privileges confined to migration. Use
node scripts/migrate-production.mjs for this authentication mode.
The chart configures none of these deployment-owned inputs. Its migration and bootstrap containers share an initialization Pod and service account, so changing database URL Secrets alone neither configures their distinct identity inputs nor enables Azure mode.
Optional operational log export
Prepare optional Collector Secrets, values, and verification through Configure platform observability.
Prepare the fresh bootstrap output PVC
Create the fresh claim, then prepare its mounted root with the controller image:
kubectl --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" \ -n openclaw-system apply -f "$OCC_INPUT_DIRECTORY/bootstrap-pvc.yaml" scripts/prepare-bootstrap-volume --kubeconfig "$KUBECONFIG_FILE" --context "$CONTEXT" \ --namespace openclaw-system --claim "$BOOTSTRAP_CLAIM" --image "$CONTROLLER_IMAGE" \ --node-selector oce-role=controlReplace --node-selector oce-role=control with the controlPlane.nodeSelector
labels, one option per label, so preparation and initialization share volume
topology.
The helper refuses any nonfresh mounted root except lost+found, schedules with
the supplied node selector before storage binds, reports Prepared bootstrap volume claim ... with UID/GID 1000 mode 0700., and retains failed Pods for
diagnosis. If policy forbids the preparation Pod, have the storage administrator
create the same root state.
Install the chart with native values:
helm upgrade --install oce deploy/helm/openclaw-enterprise \ --kubeconfig "$KUBECONFIG_FILE" --kube-context "$CONTEXT" \ --namespace openclaw-system -f "$OCC_INPUT_DIRECTORY/values.yaml" \ --wait --timeout 5mFor an explicitly published chart release
(created with publish_chart: true),
authenticate Helm, verify its receipt, then use
oci://ghcr.io/openclaw/charts/openclaw-enterprise with --version "$OCE_VERSION".
Helm owns migration and bootstrap ordering through its initialization hook. Readiness covers the API and worker probes, not authenticated API access, Agent deployment, or a model turn.
Authenticate to the production API
Retrieve initial-admin-service-key.json from the protected bootstrap PVC
through approved storage access and retain it privately. This example keeps
existing shell values and works on a session copy:
export OCC_URL="${OCC_URL:-https://<internal-occ-host>}"export OCC_BOOTSTRAP_KEY_FILE="${OCC_BOOTSTRAP_KEY_FILE:-/secure/occ/initial-admin-service-key.json}"umask 077prepare_occ_service_key() { local working_directory unset OCC_SERVICE_KEY_FILE OCC_SERVICE_KEY_DIRECTORY if [ -z "${OCC_BOOTSTRAP_KEY_FILE:-}" ] || [ -z "${OCC_URL:-}" ]; then printf '%s\n' 'Set the production URL and retained bootstrap key first.' >&2 return 1 fi if ! working_directory="$(mktemp -d /tmp/occ-service-key.XXXXXXXX)"; then printf '%s\n' 'Could not create the working key directory; stop here.' >&2 return 1 fi if ! install -m 600 "$OCC_BOOTSTRAP_KEY_FILE" "$working_directory/occ-service-key.json"; then rm -f -- "$working_directory/occ-service-key.json" rmdir -- "$working_directory" printf '%s\n' 'Could not create the working key copy; stop here.' >&2 return 1 fi if ! OCC_SERVICE_KEY_FILE="$working_directory/occ-service-key.json" occ installation get; then rm -f -- "$working_directory/occ-service-key.json" rmdir -- "$working_directory" printf '%s\n' 'Could not authenticate; the temporary key copy was removed. Stop here.' >&2 return 1 fi export OCC_SERVICE_KEY_DIRECTORY="$working_directory" export OCC_SERVICE_KEY_FILE="$working_directory/occ-service-key.json"}prepare_occ_service_keyExpect the displayed ID to match the key file's
meta.installationId. Before the first image update, use that ID to
bind upgrades to this Kubernetes Installation.
API and worker cannot read the bootstrap PVC. Keep the protected source because
initialization does not reissue a lost key. The
operator cleanup removes the
session copy.
After authentication, follow Namespace and Agent deployment, including its model-response check.
Enable GitHub browser sign-in
The published controller lacks GitHub sign-in; build a compatible image.
Follow the single-controller profile
during stopped maintenance. Install without GitHub as above, then enable it with
helm upgrade.
Activation is one-way: the database refuses older images' sessions and
auth.github must stay set. Never helm rollback past activation
(rollback);
stopped maintenance deactivates it.
-
Verify password recovery (replaceable later). Register the GitHub App callback and protect its client ID (not App ID) and secret as the reference describes. As the recovery administrator, read
data.user.idfromGET /api/auth/session. -
Create the Secret, then set
auth.github.enabled: true, that ID asauth.recoveryUserId, andagentNativeAdmin.enabled: falsein protected values, keeping workspace routing. Optionally narrowauth.github.egressCidrsor setapi.trustedProxy(settings). Profile installs set these inputs and keep them in every rerender. Rerender.bash kubectl -n openclaw-system create secret generic occ-github-login \ --from-file=client-id=/secure/occ/github-client-id \ --from-file=client-secret=/secure/occ/github-client-secret -
Close ingress. Disable automatic restarts and policy/provisioning writers; drain admitted requests.
-
Run
helm upgradewith the compatible image. The api Deployment usesRecreate, so the old Pod stops first; startup enrolls qualifying accounts, logs any it skips, and invalidates unbound sessions before serving. After a failure, keep ingress closed. -
Through restricted access, verify password recovery, new session admission, the expected Namespaces and existing Agent detail, and rejected stale sessions. Reopen ingress only after these checks, retaining one serving controller.
For enrollment, obtain the numeric subject with gh api user --jq .id as the
intended GitHub user; verify ownership through your identity process, not
email or usernames. Follow the reference's attachment and unknown-outcome handling.
Loopback tests do not qualify production stop/drain, cookies, logging, or GitHub registration.
Related
Continue with production Agent deployment, or use the production image upgrade for an existing release. For failed initialization, preserve state and follow bootstrap recovery and the production startup flow.
Connect default metrics and logs to your collectors. The optional demo stack is not recommended for production.
