Platform updates
OpenCloud installations follow one signed stable release feed. They check for a new release automatically, but installation is always a platform administrator's manual decision.
Install from a platform release
A published platform-vMAJOR.MINOR.PATCH GitHub Release contains:
compose.yml, using digest-pinned OpenCloud images and no local builds;env.exampleand the standalonegenerate-secrets.mjshelper;- the signed
release-manifest.json; - checksummed bundles for OpenCloud-owned system apps such as Docs.
The OpenCloud operator gives each installation one perpetual license username and key. The same credential reads the stable feed, downloads immutable release assets, and pulls images; it cannot publish or delete releases. On a clean Docker host, log in to the registry, place the release assets in /opt/opencloud, generate secrets once, review the public-domain and mail settings, and start Compose:
sudo install -d -m 0750 -o "$(id -un)" -g "$(id -gn)" /opt/opencloud
cd /opt/opencloud
# Values are supplied privately by the OpenCloud operator.
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.
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 --waitSet OPENCLOUD_UPDATE_FEED_URL=https://updates.opencloud.ai/v1/stable, put the perpetual key in OPENCLOUD_DISTRIBUTION_LICENSE_KEY, and keep OPENCLOUD_STACK_HOST_PATH=/opt/opencloud unless the release files live in a different absolute host directory. Never commit or publish .env. The initial bootstrap bundle is delivered by the operator; every later update is authenticated, signed, digest-pinned, and installed from the admin UI.
The updater is supported after an installation has been bootstrapped from a signed production release. Existing source-built installations use different local image names and may use a different filesystem layout. Move one of those installations to the first signed release with an operator-reviewed, one-time host migration that preserves its named PostgreSQL, MinIO, release, and function volumes. Do not ask the updater to cross that boundary: its stateful image guard intentionally rejects it.
Grant the first platform administrator
Register the user through the normal OpenCloud sign-in flow, then grant that existing identity installation-level access from the host:
docker compose --env-file .env -f compose.yml exec control-api \
node dist/platform/grant-platform-admin.js [email protected]The user can then open /admin/system/updates. The browser receives neither the bootstrap credential nor Docker access.
What happens after Update now
The control API sends a fixed apply(version) request over a private Unix socket. Only the updater and the control API mount that socket. The control API does not mount the Docker socket.
The updater launches a detached runner and records each phase in its persistent volume. The runner:
- verifies the Ed25519 release signature and approved stable version;
- checks free disk and validates the candidate Compose file;
- records the current Compose manifest and image references;
- pulls every target image before downtime;
- creates control and runtime PostgreSQL dumps;
- stops the lifecycle worker and runs the explicit migration job;
- replaces changed core services and waits for health;
- verifies
/readyz, the public route, and the exact target/version; - commits the release files and replaces the updater as the final platform container;
- reconciles bundled system apps through OpenCloud's deployment API.
The admin page reconnects after the control API restarts. Update state and logs remain under the updater_state volume even if the browser, API, updater, or host restarts.
If core readiness fails, the runner restores the previous stateless Compose release. Database migrations must therefore use expand/contract changes that remain compatible with the previous platform version. The v1 updater refuses to automatically replace PostgreSQL or MinIO images. Release CI deliberately keeps the custom PostgreSQL image digest stable when its build context is unchanged; a real database-image change therefore stops at preflight for a separate operator-managed upgrade.
OpenCloud-owned system apps
Docs is an ordinary OpenCloud application with a reserved stable app ID and slug. The release workflow builds it once and puts its complete deployment manifest, artifact URL, SHA-256 digest, byte size, and release-specific version inside the signed platform manifest.
Core services update first. Afterwards the runner deploys system apps one at a time through the same durable operation, migration, artifact, health, and atomic-activation path used by tenant apps. This gives two important outcomes:
- the currently active Docs release keeps serving traffic until the new Docs deployment succeeds;
- a Docs failure is reported as an update warning and does not roll back a healthy platform core.
A retry detects an already-active release and records success without uploading it again. Installation state is also recorded in the system_apps table, so a future admin page can show desired, deployed, and failed system-app versions.
To add another OpenCloud-owned app, add one fixed definition and its no-shell prepare commands to infra/system-apps.json, and keep its opencloud.yaml app ID and slug stable. The release workflow prepares and packages every listed app. Do not put customer apps in this list; their deployment cadence remains independent of platform releases.
Health and version surfaces
The control plane exposes:
/livezfor process liveness;/readyzfor database connectivity and current schema migrations;/versionfor platform version, Git revision, build time, and release ID;opencloud_build_infoin Prometheus with version and revision labels.
The worker health check requires a database heartbeat from the current container instance and platform version less than 30 seconds old. The updater verifies both container health and exact version identity; a generic HTTP 200 is not enough to commit an update.
Publishing a stable release
OpenCloud keeps source and release review in the private platform repository while serving deployable artifacts from the separately deployed distribution stack:
registry.opencloud.aistores digest-pinned container images on local disk;updates.opencloud.aistores signed release manifests, bootstrap assets, release notes, and system-app bundles on local disk;- the private GitHub Release keeps the exact release assets as an off-host backup and an operator-visible history.
Pushing a semantic platform-v*.*.* tag runs .github/workflows/platform-release.yml. The workflow tests the repository, builds linux/amd64 images directly into the authenticated registry, records their digests, builds system-app bundles, signs the release manifest, verifies the complete asset set, and creates a draft Release in the private source repository. It does not change the stable feed.
Configure these private-repository values before tagging:
- secret
PLATFORM_RELEASE_PRIVATE_KEY: the Ed25519 private signing key in PEM form; - variable
PLATFORM_RELEASE_PUBLIC_KEY: its matching public PEM key; - secrets
DISTRIBUTION_REGISTRY_USERNAMEandDISTRIBUTION_REGISTRY_PASSWORD: publisher access to the registry; - secret
DISTRIBUTION_PUBLISH_TOKEN: publisher access to the release server; - secrets
DISTRIBUTION_LICENSE_USERNAMEandDISTRIBUTION_LICENSE_KEY: the perpetual subscriber credential used for end-to-end verification.
Publishing the reviewed private draft is the deliberate promotion step. The release-published workflow downloads the backed-up asset set, verifies its signature and self-hosted URLs, verifies every image digest as the publisher, uploads the assets to the release server, atomically moves the signed manifest to /v1/stable, and finally verifies all images with the pull-only license. Publication is idempotent: a retry accepts byte-identical assets but cannot overwrite an existing version.
The distribution stack lives in infra/distribution/compose.yml as a separate opencloud-distribution Compose project. Its gateway alone joins opencloud_edge; the registry and release server stay on an internal network and expose no host ports. The main Caddy routes the two wildcard-backed hostnames to that gateway. Raw credentials stay in Infisical and GitHub Actions; the distribution host stores only bcrypt or SHA-256 hashes.
CI never connects to customer installations. Each installation checks the signed feed independently and installs only after its own platform administrator clicks Update now.