OpenClaw EnterpriseDOCSGitHub

Compose development startup

Trace host preflight, database initialization, and API/worker startup. See the parent flow for its context and overall sequence.

Overview

scripts/dev-up selects Docker or Kubernetes Compute and starts the requested development topology from a checkout. Docker Compute and Compose-backed Kubernetes profiles run PostgreSQL, migration, bootstrap, the API, and the worker in Compose. The explicitly selected Kubernetes-only profile runs those services in the owned k3d cluster. This flow ends after authenticated Installation and bootstrap Namespace readiness; OpenShell also requires Workspace readiness.

Entry Points

Flow

graph TD
  A["<b>Start development</b><br/>scripts/dev-up"] --> B{"<b>Compute Driver</b><br/>Docker or Kubernetes"}
  B -- "Docker" --> C["<b>Render Compose</b><br/>Validate publications"]
  B -- "Kubernetes" --> D{"<b>Control plane</b><br/>Compose or Kubernetes"}
  D -- "Kubernetes" --> E["<b>Owned k3d stack</b><br/>PostgreSQL and OCE"]
  D -- "Compose" --> F["<b>Hybrid stack</b><br/>Compose OCC and k3d Compute"]
  C --> G["<b>Prove Installation</b><br/>Authenticated service key"]
  F --> G
  E --> G
  F -- "OpenShell" --> H["<b>Own Workspace</b><br/>OpenShell operator mode"]
  E -- "OpenShell" --> H
  G --> I["<b>Record cleanup</b><br/>Exact engine and resources"]
  H --> I

Execution trace

1. scripts/dev-up: host preflight and runtime image selection

scripts/dev-up:require_command, internal/occdev/compose.go:AnalyzeCompose, deploy/runtime/Dockerfile

The helper requires the checkout-local bin/occ from pnpm cli:build, accepts --key-output, and forwards arguments after -- to Compose. This section traces the default OCC_DEVELOPMENT_COMPUTE_DRIVER=docker. OpenShell requires Kubernetes Compute. Kubernetes Compute also defaults to the Compose control plane; explicitly select OCC_DEVELOPMENT_CONTROL_PLANE=kubernetes for local Kubernetes-only development.

The Docker Compute path first probes a running Docker Engine and the JSON configuration capability required from Docker Compose. If that probe fails, it selects podman directly; a docker compatibility alias is neither required nor treated as Docker merely because of its name. Podman requires the standalone podman-compose provider and yq v4; the helper pins that provider so status and stopped one-shot container behavior stay consistent.

Docker Compose supplies resolved JSON directly. Podman Compose supplies YAML, which dev-up converts to JSON inside its private temporary directory before passing it to ./bin/occ dev analyze-compose. The shared Go analyzer enforces loopback controller and database publications and resolves the Docker runtime image selection. The helper appends compose.podman.yaml last so the worker receives Podman's reported API socket at /var/run/docker.sock. The base Docker Compute worker disables SELinux process labeling because relabeling a host engine socket is unsafe; other services retain SELinux confinement. The override also gives migration, bootstrap, API, and worker one shared development image. Because podman-compose otherwise rebuilds that identical target once per service, dev-up builds it once through the migration service and starts the stack with --no-build. Docker keeps its native up --build path. Expanded configuration and credentials are never printed.

On macOS, run podman as your normal host user, without sudo. The k3d workflow requires a rootful Podman machine; rootful describes the VM, not running podman as root on the host.

If neither a shared runtime image nor separate gateway/Agent images are set, the helper selects openclaw-enterprise-runtime:quickstart for this invocation. It builds that default image with deploy/runtime/Dockerfile and the repository-root context only when the image is missing. Custom image references must already exist; an incomplete custom selection fails before startup is reported successful.

Existing tags are reused even after the runtime recipe changes. Operators rebuild and verify the image explicitly to pick up package changes. The runtime recipe owns packaged channel plugins and gateway/Codex compatibility checks; dev-up does not install missing plugins or verify a model turn.

2. compose.yaml:services.postgres and services.migrate

compose.yaml:services.postgres, compose.yaml:services.migrate

Compose starts PostgreSQL first and keeps its data in the local occ_postgres_data volume. Compose also declares occ_configuration_data, but mounts it only into the controller for development Configuration documents. The PostgreSQL service uses local-only administrator credentials to initialize the database and the checked-in local SQL to create the less-privileged occ_migrator and occ_app roles.

The migration service waits for PostgreSQL, connects with OCC_MIGRATION_DATABASE_URL, and applies Drizzle migrations. The API and worker never use the migrator or PostgreSQL administrator URL. dev-up invokes this through Compose; it does not run migration directly.

3. Initialize before starting the API or worker

compose.yaml:services.bootstrap, scripts/bootstrap-installation.mjs

After migration exits 0, Compose runs the shared initializer with development inputs. Fresh bootstrap creates the development human administrator, the non-Agent service administrator, the singleton Installation, native IAM seed, audit evidence, and the initial service-key response. It also creates the initial default Namespace in provisioning state and queues worker reconciliation; the worker later provisions its backing Docker boundary. Existing Installations retain their Namespaces, accounts, keys, IAM policy, configuration, and revision history. Missing, expired, or revoked keys do not trigger another bootstrap issue.

Only the initializer mounts occ_bootstrap_data; the API and worker load committed state after initializer success. The bootstrap flow owns credentials, concurrent attempts, and failure recovery.

4. The API admits only local development traffic

apps/controller/src/server.mjs:start, apps/controller/src/composition/development-postgres.ts:createDevelopmentConfigurationDriver, apps/controller/src/drivers/configuration/filesystem/index.ts:FilesystemConfigurationDriver

The API starts in NODE_ENV=development, binds inside the Compose network, and publishes its host port only on 127.0.0.1. OCC_AUTH_SECRET signs user sessions and OCC_AUTH_BASE_URL fixes the cookie origin.

Development accepts the explicitly configured Compose bridge CIDR as local control-plane traffic, while non-loopback clients, forwarded headers, caller-supplied identity headers, bearer credentials, and trusted-proxy claims remain rejected. The API uses the application-role PostgreSQL URL and never receives the container-engine socket.

After the controller health check passes, dev-up waits for the worker readiness probe before copying the initializer-owned service-key JSON from the stopped bootstrap container. Docker Compose performs the Docker copy. Because podman-compose has no cp command, the helper identifies exactly one scoped bootstrap container from Compose status labels and invokes podman cp by ID. --key-output must name an absent destination in a private operator-owned directory; otherwise the helper creates a private temporary directory. The helper never overwrites an existing local file, never prints data.key, and never reruns bootstrap to replace a missing key.

dev-up then reads the Installation with ./bin/occ installation get and the copied service-key response. apps/controller/src/auth/index.ts:ControllerAdmissionVerifier.verify validates the x-api-key and maps it to the Installation-scoped service administrator; current IAM policy still authorizes each resource operation. The startup proof succeeds only when the returned resource ID matches the copied key response's meta.installationId. An invalid, expired, or revoked key fails with 401 without cookie fallback. The service-key flow owns admission details.

When OCC_CONFIG_PATH is absent, PostgreSQL-backed development selects the filesystem Configuration Driver from OCC_DEVELOPMENT_CONFIGURATION_ROOT. Compose sets that root to /app/.development/configurations and backs it with the occ_configuration_data named volume. PostgreSQL remains the OCC metadata system of record; native Configuration documents live in that driver-owned volume.

5. The worker selects Docker compute and claims durable work

apps/controller/src/worker.mjs:configuration

The worker starts after the controller is healthy with the same application-role OCC_DATABASE_URL. When OCC_CONFIG_PATH is absent in development, it selects compute-docker-development with implementation docker-local. Setting OCC_CONFIG_PATH explicitly selects the trusted Driver set described by that file instead.

The worker loads the singleton Installation, validates persisted IAM policy, and polls the PostgreSQL work queue. Startup readiness means the worker can claim durable work; it does not mean an Agent, AgentRevision, or TUI exists. Every claimed operation reauthorizes the original actor before calling Compute. The worker is the only Compose service with Docker-compatible engine access. It does not mount the configuration volume.

Kubernetes-only startup

internal/occdev/openshell_k3d.go:upK3d, internal/occdev/gateway_k3d.go:installDevelopmentRoutingControllers, internal/occdev/repository_k3d.go:enableDevelopmentRepository.

The Kubernetes-only profile creates its owned k3d cluster in K3s legacy iptables mode, imports matching OCE images, and runs PostgreSQL, migration, bootstrap, API, and worker inside Kubernetes. An explicitly selected IPv4 resolver replaces k3d's node DNS rewriting; the host resolver is unchanged.

Without OpenShell, it verifies the pinned cert-manager and Envoy Gateway manifests. It waits for the k3s-owned Gateway API CRDs to be created and established before installing Envoy, and prints k3s add-on status before rollback if that wait fails. Before configuring gateway proxy trust, internal/occdev/network_k3d.go:verifyDevelopmentNetworkPolicy checks allowed and denied direct Pod traffic with credential-free Pods and a temporary policy. After bootstrap creates the initial Gateway Namespace, it repeats the checks against the Driver's actual policies. Probe Pods use short graceful shutdowns and are deleted with UID preconditions. Cleanup waits for the selector-matching Pods before removing their egress policy; any failed probe or cleanup prevents startup success. This is a point-in-time, single-node check, not continuous enforcement.

Before writing the Installation, scripts/prepare-development-codex-seccomp.mjs probes the imported runtime in a credential-free Pod on the owned node. If RuntimeDefault blocks the sandbox, the shared reviewed generator derives a content-addressed Localhost profile from that node's effective policy. Startup verifies the loaded policy, workspace and outside-write behavior, and missing profile failure before selecting it for dedicated Codex containers. Failure rolls back the owned cluster; the state directory records nonsecret provenance.

Startup waits for the Gateway, certificate, and proxy Pods before reporting success. Envoy source addresses must fall inside the selected node's Pod CIDR; the tenant ingress policy must still admit only the Gateway's exact proxy peer.

The loopback development proxy also terminates browser HTTPS using a private per-installation CA and a leaf limited to that installation's console and Agent hosts. The API and browser NodePorts are published only to host loopback. The bootstrap administrator password, service key, and CA private key remain in the private state directory. Browser CA trust is an explicit operator action.

If repository inputs are selected, startup creates the scoped broker after the actual initial Namespace exists and waits for authenticated repository-option discovery. This does not prove a model turn, native sandbox, or Git operation.

12. Select Kubernetes development and preserve cleanup ownership

internal/occdev/command.go:selectEngine, internal/occdev/command.go:pinEndpoint, internal/occdev/command.go:podmanEndpoint, internal/occdev/up.go:Up.

The ordinary Kubernetes lifecycle, with no Sandbox Driver selected, chooses Docker or Podman and resolves a host-reachable Unix socket: Docker from the active context, and Podman from podman machine inspect whenever its reported socket exists only inside a virtual machine. It records that socket with the Compose project and generated occ-dev-* cluster name in a private state directory. Cleanup validates that state and reuses the recorded endpoint. Changing the active Docker context after startup does not redirect cleanup to another engine.

Startup refuses existing cluster or project resources, validates the resolved Compose publications through internal/occdev/compose.go:AnalyzeCompose, and rejects external or unscoped networks and volumes through internal/occdev/up.go:validateResourceOwnership. It then claims the state directory with an exclusive 0700 creation. It writes the rendered Compose snapshot privately before creating resources. setKubernetesBridgeGateway preserves an explicit development-network gateway or derives the first usable address from its rendered subnet before saving the snapshot. This supplies the bridge gateway k3d requires, including when the operator overrides the subnet. Startup and cleanup both use that snapshot, so later .env edits cannot change the saved project configuration.

With OCC_DEVELOPMENT_CONTROL_PLANE=kubernetes, Kubernetes Compute branches before Compose rendering into internal/occdev/openshell_k3d.go:upK3d. That profile uses the engine only for k3d and image operations, and adds OpenShell only when selected. Without OpenShell, the Installation selects the bundled Presets and curated Codex Plugin Driver. Startup copies the generated administrator password and service key into the private state directory.

When repository inputs are selected, internal/occdev/repository_k3d.go first validates their private directory, explicit Namespace placeholder, profiles, App key, and approved egress IPv4 /32 endpoints. After authenticated bootstrap and Namespace readiness, it substitutes the server-assigned Namespace ID into the immutable registry, generates a CA and exact-host certificate, creates separate Kubernetes inputs, and upgrades Helm with the selected Repo Driver and worker sidecar. Startup compares authenticated repository discovery with the approved references and profiles. It does not perform Git operations; see the local repository procedure.

The default OCC_DEVELOPMENT_CONTROL_PLANE=compose continues through the Compose snapshot and startup sequence. An unsupported control-plane value fails before resource creation. The OpenShell provisioning flow owns both OpenShell control-plane sequences.

13. Bootstrap OCC, create k3d, and prepare runtime configuration

internal/occdev/up.go:Up, internal/occdev/up.go:waitCompleted, internal/occdev/kubernetes.go:writeKubeconfigs, internal/occdev/kubernetes.go:importRuntime, internal/occdev/kubernetes.go:writeInstallation, internal/occdev/openshell.go:prepareOpenShell.

Compose starts PostgreSQL, migration, and bootstrap. The lifecycle waits for successful migration and bootstrap exits before creating the dedicated k3d cluster on the Compose network. With the default Sandbox profile, OCC_DEVELOPMENT_K3S_IMAGE selects the node image; its default +v1.35 resolves the latest K3s patch in the 1.35 family. An explicit image avoids the channel lookup. OpenShell uses its pinned image in both control-plane modes. The cluster API binds host loopback; creation leaves the default kubeconfig and current context unchanged.

The host kubeconfig remains owner-readable. The container kubeconfig uses the cluster's internal load-balancer hostname with TLS verification. The lifecycle imports the selected local runtime image, resolves its in-cluster digest, and writes Installation configuration selecting Kubernetes Compute, Configuration, and Secret Drivers with native IAM. Its runtime section configures the transport Secret prefix and gateway storage class accepted by the current Compute Driver schema. Generated Gateway and Harness resource limits allow 2 GiB of memory per workload; the current runtime can exceed the former 1 GiB limit during startup. The container configuration and kubeconfig are individually readable by non-root containers, behind the private host directory, and mounted read-only into the API and Kubernetes worker. Neither service receives the engine socket.

When Compose mode also selects OpenShell, startup installs the pinned Agent Sandbox controller and OpenShell Gateway in k3d before starting the API and worker. The Gateway uses openshell-system and a fixed NodePort reachable from the private Compose network. The generated Sandbox Driver configuration selects operator workspace mode and includes the rendered workspace-chart resources that ensureNamespace applies for each OCC Namespace.

14. Prove readiness and clean up the owned Kubernetes profile

internal/occdev/up.go:waitReady, internal/occdev/up.go:copyAndVerifyKey, internal/occdev/down.go:Down, internal/occdev/down.go:cleanup, internal/occdev/state.go:readState.

The lifecycle starts the API and Kubernetes worker, waits for API health and worker readiness, copies bootstrap output to a private temporary file, and uses occclient to read the Installation. Its ID must match the bootstrap response before the final key file is written exclusively. With OpenShell, startup also waits for the bootstrap Kubernetes Namespace and then for OCC to report that Namespace ready, which establishes that the Sandbox Driver created or adopted its operator-mode Workspace.

On failure, startup attempts resource cleanup. Explicit Kubernetes shutdown validates the marker, state, and Compose snapshot before using the recorded engine endpoint. Cleanup stops the API and worker before deleting the named k3d cluster and Compose project volumes. It continues cleanup after individual errors and retains state when any cleanup step fails. Complete cleanup removes the state directory and its helper-owned key. A successfully returned external --key-output file remains operator-owned; startup removes a newly written external key if a later OpenShell readiness step fails.

Debugging and Verification

Search documentation