Skip to content

Platform updates

OpenCloud installations follow one licensed stable release feed. They check for a new release automatically, but installation is always a platform administrator's manual decision.

For the hosted service, test changes in an isolated branch preview under <branch-slug>.ocd.dev, then merge the accepted change to main. Only the exact current main revision may receive a platform-vMAJOR.MINOR.PATCH tag. The tag workflow runs the release-only gates and creates no draft unless they all pass. Never source-build main into the opencloud.ai production Compose project.

The repository's opencloud-platform CLI is the canonical deployment interface for operators and coding agents. It uses a named, inspectable SSH target and preserves the production safety checks. Direct Compose commands, updater-socket calls, and browser automation are not substitutes for repository-driven production deployments.

sh
# Configure ~/.config/opencloud/preview.env once with PREVIEW_USER_EMAIL.
# PREVIEW_USER_PASSWORD is optional and defaults to ais7sudh.
npm run preview:branch -- check
npm run preview:branch

The preview launcher records only the configured email in its ignored branch environment, creates or updates that confirmed user and password, and reports the login page with password access and both tester fields prefilled. Anyone who can load that page can inspect the credentials, so previews use synthetic test data only. Branch previews are the platform test environment. The former opcl.app staging deployment is retired and must not be used as a fallback, acceptance target, or release gate. Staging-named CLI and configuration artifacts remain only for compatibility and isolated regression coverage.

After preview acceptance and merge, publish and deploy the exact release:

sh
opencloud-platform deploy production --version MAJOR.MINOR.PATCH

Production still requires an immutable semantic release from the accepted main revision; preview source is never installed directly into production.

What OpenCloud publishes

Only these OpenCloud-owned images are stored in the licensed registry:

  • platform-control-plane
  • platform-agent-service
  • platform-agent-launcher
  • platform-ai-gateway
  • platform-agent-runtime
  • platform-agent-app-owner-runtime
  • platform-codex-inference-runtime
  • platform-functions
  • platform-updater

Every image for a release uses the same SemVer tag, for example :0.6.0. Published SemVer tags must never be overwritten. release.json records the exact digest for each component. Compose pins all runtime components by digest except platform-updater: already-installed schema-v2 updaters require its SemVer handoff name, so CI verifies that immutable alias resolves to the recorded updater digest.

The released compose.yml references public upstream images directly for services such as Caddy, PostgreSQL, PostgREST, Kong, MinIO, Prometheus, Loki, Promtail, Grafana, exporters, and cAdvisor. Their configuration is embedded in the standalone Compose file. They are not copied into the private registry.

A published private GitHub Release contains:

  • the managed compose.yml baseline;
  • env.example and generate-secrets.mjs;
  • the direct release.json stable-feed document;
  • release notes; and
  • bundled system-app metadata and archives.

The distribution license is the access boundary. It permits an installation to read the stable feed and protected release assets and pull licensed images. It cannot publish releases or push images. Release notes remain public.

Install from a platform release

On a clean Docker host, put the approved release assets in /opt/opencloud, log in to the registry, generate secrets once, and review .env before starting the stack:

sh
sudo install -d -m 0750 -o "$(id -un)" -g "$(id -gn)" /opt/opencloud
cd /opt/opencloud

printf '%s' "$OPENCLOUD_DISTRIBUTION_LICENSE_KEY" | \
  docker login registry.opencloud.ai \
    --username "${OPENCLOUD_DISTRIBUTION_LICENSE_USERNAME:-license}" \
    --password-stdin

# Download compose.yml, env.example, and generate-secrets.mjs from one
# approved release before continuing.
docker run --rm \
  --user "$(id -u):$(id -g)" \
  --volume "$PWD:/work" \
  --workdir /work \
  node:22-alpine \
  node generate-secrets.mjs env.example .env

# Edit .env before starting production.
docker compose --env-file .env -f compose.yml up -d --wait

Keep these defaults unless the stack is installed elsewhere:

dotenv
OPENCLOUD_UPDATE_FEED_URL=https://updates.opencloud.ai/v1/stable
OPENCLOUD_STACK_HOST_PATH=/opt/opencloud
OPENCLOUD_COMPOSE_OVERRIDES=compose.local.yml

Store the perpetual key in OPENCLOUD_DISTRIBUTION_LICENSE_KEY. Never commit or publish .env.

The updater is supported after an installation has been bootstrapped from a schema-v2 licensed production release. A source-built installation or one still using the retired signed schema-v1 feed needs a one-time operator-reviewed migration that preserves its Compose project name, .env, and named data volumes. The old updater cannot consume the new feed format, so do not publish the first schema-v2 release until every existing production installation has an approved bootstrap plan.

Extend the released stack

/opt/opencloud/compose.yml belongs to the updater. Do not edit it directly; the next release replaces it. Put deployment-specific changes in the optional /opt/opencloud/compose.local.yml instead:

yaml
services:
  platform-grafana:
    environment:
      GF_USERS_ALLOW_SIGN_UP: "false"

  site-exporter:
    image: example/site-exporter:2.1
    restart: unless-stopped
    networks:
      - platform-observability

An override may change default services, add arbitrary public or private images, use local build sections, define new networks and volumes, or add deployment-only services. The updater validates only the final Compose syntax. Docker and Compose remain responsible for whether those definitions can run.

For an attempt-fenced Agent boundary release, the updater additionally pins the full rendered definitions of agent-migrate, agent-service, agent-launcher, agent-runtime, and agent-app-owner-runtime to the release contract, including their exact image digests. Remove every override of those five services before the cutover. Other deployment-only services and settings on noncritical services remain supported. Treat the override set as locked for the whole update: do not edit, add, remove, or reorder override files until the operation has succeeded or its documented fail-forward recovery has settled.

For multiple files, list their paths in merge order:

dotenv
OPENCLOUD_COMPOSE_OVERRIDES=compose.site.yml,compose.monitoring.yml

Paths must be relative .yml or .yaml files inside /opt/opencloud. Missing optional files are ignored. For every operation, the updater loads the managed baseline first and every existing override afterwards, using standard Docker Compose merge rules.

To apply an operator change immediately, run the same merged stack explicitly:

sh
cd /opt/opencloud
docker compose --project-name opencloud --env-file .env \
  -f compose.yml -f compose.local.yml \
  up -d --build --wait

If compose.local.yml does not exist, omit its -f argument. Removing a service from an operator file does not delete its old container automatically; stop and remove that named service explicitly after reviewing its volumes.

What happens after Update now

The control API sends a fixed apply(version) request over a private Unix socket. The control API never mounts the Docker socket. The updater starts a detached runner and records progress in its persistent volume so API, updater, browser, or host restarts do not lose the operation record.

The managed baseline, rather than an operator override, selects the privileged updater used for the release. The runner validates the exact registry, repository, release tag, and updater protocol version before explicitly pulling that image.

The runner:

  1. fetches the approved release.json with the installation license and checks updater compatibility and free disk space;
  2. downloads the new managed compose.yml and validates it together with all existing operator overrides;
  3. pulls images for every profile and prepares every local build before downtime; an Agent boundary release also freezes the accepted merged model in a root-only mode-0600 snapshot so later override edits cannot change the migration candidate;
  4. when the approved updater image differs from the current runner, starts the already-pulled target image with network pulling disabled, releases the old runner, and repeats preflight in the target updater;
  5. creates complete control and runtime PostgreSQL dumps as the database administrator;
  6. stops the lifecycle worker;
  7. reconciles the complete merged stack with docker compose up -d --wait, including the one-shot migration dependencies;
  8. verifies container health, the public route, and the exact target /version; an Agent boundary additionally verifies the images that actually ran, Agent readiness, migration 052, the new two-argument harness function, and retirement of the old one-argument signature;
  9. stops services removed by the released baseline, without deleting their volumes;
  10. atomically commits the new managed baseline and release.json; and
  11. deploys bundled system apps through OpenCloud.

The updater does not use --remove-orphans, so it never performs an implicit container or data cleanup. Operator overlays survive unchanged and cannot replace the target handoff runner.

The target handoff moves backup and migration behavior onto the release being installed, preventing an older updater from failing on database objects added by a newer release. The first release that introduces this mechanism must remain installable by the previously published updater; after that transition, the handoff is part of the release regression suite.

If reconciliation or core verification fails, the runner stops services that were newly introduced by the candidate, restores the previous managed baseline, reapplies the same overrides, and verifies the previous version. Database migrations must therefore follow expand/contract rules and remain compatible with the previous release. Database dumps are recovery artifacts; automatic rollback does not restore them.

The attempt-fenced Agent boundary is deliberately non-rolling. Before its pending marker is durable, a failed preflight may restore only the digest-proven previous baseline. A retry with a prepared but uncommitted transition reconciles that baseline before beginning new preflight. After the marker exists, the updater never starts pre-boundary Agent or launcher binaries: it retains the approved target and fails forward with the same-boundary release or a newer approved repair. Repair generations preserve the first marker committedAt and add completedAt only after the exact images, Agent readiness, migration 052, and function-shape postconditions pass.

A bundled system-app failure becomes a warning after the platform core is healthy. It does not roll back the core stack.

Change the default Compose file

A change intended for every installation belongs in infra/compose.yml and is delivered in the next platform release:

  • For a public dependency, reference its upstream tagged image and add its configuration directly to Compose. No registry publication is needed.
  • For another OpenCloud-owned executable, add its build to the release image matrix, assign the same platform version tag, teach the production renderer and bundle verifier about it, and include it in the licensed registry access tests.
  • For a deployment-specific service, use an operator override instead of changing the managed baseline.

When the release is installed, the full-stack reconcile creates the new service automatically. A service removed from the managed baseline is stopped only after the replacement stack passes verification. Named volumes are retained for deliberate operator-led cleanup.

Grant the first platform administrator

Register through the normal sign-in flow, then grant that existing identity installation-level access from the host:

sh
cd /opt/opencloud
docker compose --env-file .env -f compose.yml exec control-api \
  node dist/platform/grant-platform-admin.js admin@example.com

The user can then open /admin/system/updates. The browser receives neither the distribution license nor Docker access.

Manage well-known responses without a release

The same platform-admin page manages small static responses under /.well-known/ on the base, API, Auth, and MCP origins. Use it for domain verification tokens and association documents whose values change independently of OpenCloud code. Choose the origin and filename, select plain text or JSON, and enter the exact response body. Publishing, editing, and removing an entry updates the public URL immediately; no image build, platform release, or service restart is involved.

Responses use Cache-Control: no-store, are limited to 64 KiB, and are recorded in the installation audit log by content digest rather than body. Plaintext responses are sent byte-for-byte without an added newline or JSON wrapper. JSON entries must parse before they are accepted. OpenCloud's OAuth authorization-server and protected-resource discovery names are reserved and cannot be replaced through this manager.

The first platform version containing this manager still has to be installed normally. After that one-time installation, platform administrators should use the manager for value rotations instead of producing another release.

For production, initiate and monitor repository-driven updates with the opencloud-platform CLI and an explicitly registered production SSH target:

sh
opencloud-platform deploy production --version MAJOR.MINOR.PATCH

The CLI verifies the target control-plane container reports the production environment and opencloud.ai domain before it calls the same audited admin update API. It carries no platform credential over SSH, follows the durable operation across service restarts, and verifies the exact public version, release ID, and commit. See the repository's cli/platform/README.md for server registration and setup.

The browser page at https://opencloud.ai/admin/system/updates remains an interactive human fallback. Coding agents must use the CLI rather than browser automation.

Do not use an ad hoc host-side Docker command as a production-targeting mechanism. It acts on whichever daemon is connected and lacks the CLI's saved target identity and runtime boundary checks. With either supported flow, compare the final result with https://api.opencloud.ai/version.

Publish a stable release

Before tagging, confirm the accepted change passed its branch preview and was merged. Wait for .github/workflows/platform-image-candidates.yml when practical, then push a new platform-vMAJOR.MINOR.PATCH tag from the exact current main commit. The candidate workflow runs no tests and grants no release approval; it only precomputes changed image digests.

.github/workflows/platform-release.yml verifies exact main, then runs the repository checks, onboarding journey, image work, and package preparation in parallel on the persistent opencloud-ci self-hosted runner. It builds only changed linux/amd64 images, reuses prior digests for unchanged components, creates all nine immutable SemVer aliases, renders and verifies the standalone baseline, and creates a private draft GitHub Release only after every gate passes. It does not change the stable feed. The former automatic PR workflows are manual-only diagnostic tools; release CI is the final automated gate.

The repository needs these GitHub Actions secrets:

  • DISTRIBUTION_REGISTRY_USERNAME
  • DISTRIBUTION_REGISTRY_PASSWORD
  • DISTRIBUTION_PUBLISH_TOKEN
  • DISTRIBUTION_LICENSE_USERNAME
  • DISTRIBUTION_LICENSE_KEY

Publishing the reviewed draft triggers .github/workflows/platform-promote-stable.yml. The workflow checks that each of the nine version tags resolves to its approved digest using publisher credentials, uploads the exact release assets, atomically advances /v1/stable, and verifies the tags again using the pull-only subscriber license. Published release directories are immutable; retrying with byte-identical assets is allowed, but changing an existing version is rejected.

registry.opencloud.ai stores only the nine OpenCloud-owned images. updates.opencloud.ai stores the licensed baseline, direct release metadata, bootstrap assets, notes, and system-app bundles. The private GitHub Release is the off-host copy and review history. Publishing never connects to a customer installation. Retired wrapper-image manifests from older releases remain in registry storage until a separate, operator-approved garbage-collection operation removes them.

Self-hosted infrastructure for agent-built applications.