Skip to Content
InfrastructureLocal Development

Local Development

The whole platform runs locally from one docker-compose stack: infrastructure/local/docker-compose.yml. It brings up the three per-solution Orbit apps, Aurora (the portfolio control plane), and the portfolio registry (Postgres plus three Hono services) on a private bridge network, bind-mounting each app’s codebase for hot reload and a reference solution repository for the agent to author against.

cd infrastructure/local cp .env.example .env # then edit values (paths, ports, credentials) docker compose up --build

All services join the orbit-net bridge network, so containers reach each other by service name (e.g. http://orbit-agent:3000 from inside another container). Each service also publishes a host port so the browser can hit it directly.

Topology

Ports shown are the host-side defaults; every container listens on its own fixed internal port (3000, except the agent-webapp on 3001 and Postgres on 5432).

Services

ServiceBuild (context → Dockerfile)Host port (env var, default)Container portDepends on
orbit-webappimplementation/orbit-webapp/dockerfiles/Dockerfile.localORBIT_WEBAPP_PORT, 30003000orbit-agent
orbit-agentimplementation/maxq-orbit-agent/dockerfiles/Dockerfile.localAGENT_PORT, 30103000
orbit-agent-webappimplementation/maxq-orbit-agent-webapp/codebase../dockerfiles/Dockerfile.localAGENT_WEBAPP_PORT, 30013001orbit-agent
aurora-webappimplementation/aurora-webapp/codebase../dockerfiles/Dockerfile.localAURORA_WEBAPP_PORT, 30403000
postgresimage postgres:17-alpinePOSTGRES_PORT, 54325432
customer-serviceimplementation/customer-service/dockerfiles/Dockerfile.localCUSTOMER_SERVICE_PORT, 30213000postgres (healthy)
tenant-serviceimplementation/tenant-service/dockerfiles/Dockerfile.localTENANT_SERVICE_PORT, 30223000postgres (healthy)
solution-serviceimplementation/solution-service/dockerfiles/Dockerfile.localSOLUTION_SERVICE_PORT, 30233000postgres (healthy)
activity-serviceimplementation/activity-service/dockerfiles/Dockerfile.localACTIVITY_SERVICE_PORT, 30243000postgres (healthy)

Notes on the build contexts:

  • orbit-webapp, orbit-agent, the three registry services, and the activity service build with implementation/ as the context so their file: dependencies — the shared shared/trajectory-loader, shared/registry-kit, and shared/activity-kit packages — are inside the build context and get baked into the image.
  • The registry services and the activity service run their own migrations on boot, so up from zero yields a working empty registry and activity feed; all four wait for the Postgres healthcheck (pg_isready) before starting.

The volume and mount model

Codebase bind mounts + anonymous volumes

Every Node service follows the same pattern, spelled out in the Dockerfile.local files:

  1. Dependencies are installed at image build time (npm ci / npm install).
  2. At runtime the codebase folder is bind-mounted into the container, so saved file changes hot-reload via next dev / tsx watch.
  3. Anonymous volumes are declared on node_modules (and .next for the Next.js apps) — e.g. - /app/.next — so the bind mount doesn’t shadow what the image installed. Without them you’d get macOS-host node_modules inside a Linux container.

For orbit-webapp and orbit-agent the in-container layout mirrors the repo’s implementation/ folder (/app plays that role): the app lives at /app/<name>/codebase and the shared @maxq/trajectory-loader package is baked in at /app/shared/trajectory-loader, so the file:../../shared/... symlink in node_modules resolves. Changing a shared package therefore requires an image rebuild — see the workflows below.

The solution mounts (orbit-agent is the sole owner)

The agent is the single writer and sole owner of the solution-repo mounts. Since the 2026-07-02 refactor, orbit-webapp has no repository mount at all — it fetches all solution and sources data over HTTP from the agent’s in-memory model (ORBIT_AGENT_URL: http://orbit-agent:3000, a docker-network name; the browser never calls the agent directly for this).

Host path (env var)Mounted atModePurpose
SOLUTION_REPOSITORY_PATH (required)/reporead-writeThe customer solution repository; its solution root is workspace/solution-definition/.
SOLUTION_INTERNAL_REPOSITORY_PATH (defaults to the sibling SOLUTION_REPOSITORY_PATH + -internal)/repo-internalread-writeThe matching internal repo: agents, catalog, templates, .orbit/ audit trail.
TRAJECTORY_METHODOLOGY_PATH (defaults to a sibling trajectory-methodology)/methodologyread-onlyThe global methodology repo — consulted only by the methodology-upgrade operation; normal authoring resolves the copy vendored inside the customer repo.

The agent reads these in-container paths from its own environment (SOLUTION_REPOSITORY_PATH=/repo, etc.); the host paths are chosen purely via .env.

Aurora’s mounts

Aurora manages the fleet live from GitHub, so it mounts no solution repo. It gets two read-only mounts instead:

  • the platform repo root at /repo-platform:ro — the in-product chat agent’s working directory (AURORA_AGENT_CWD, default /repo-platform), so the agent sees the real workspace root;
  • a secrets folder at /secrets:roAURORA_SECRETS_PATH (default: the git-ignored implementation/aurora-webapp/codebase/secrets/), holding the two GitHub App private-key .pem files.

Named volumes (survive down, die on down -v)

VolumeAttached toHolds
orbit-claude-sessionsorbit-agent at /root/.claude/projectsClaude Code session transcripts — required to resume: failed/interrupted tasks across container recreates.
orbit-pg-datapostgres at /var/lib/postgresql/dataThe portfolio registry database.

Environment configuration

Copy .env.example to .env in infrastructure/local/ and fill it in. The variable names and what they point at (never commit values):

VariableWhat it configures
ORBIT_WEBAPP_PORT, AGENT_PORT, AGENT_WEBAPP_PORT, AURORA_WEBAPP_PORT, CUSTOMER_SERVICE_PORT, TENANT_SERVICE_PORT, SOLUTION_SERVICE_PORT, ACTIVITY_SERVICE_PORT, POSTGRES_PORTHost-side published ports (defaults 3000 / 3010 / 3001 / 3040 / 3021 / 3022 / 3023 / 3024 / 5432).
SOLUTION_REPOSITORY_PATHRequired. Host path of the customer solution repo (must contain workspace/solution-definition/solution.yaml); mounted at /repo in orbit-agent only.
SOLUTION_INTERNAL_REPOSITORY_PATHHost path of the internal repo; defaults to the customer repo’s path with -internal appended.
TRAJECTORY_METHODOLOGY_PATHHost path of the global methodology repo (read-only mount).
ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKENThe orbit-agent’s Claude credential — set exactly one (API key vs Claude Code subscription token).
DEFAULT_MODEL, LOG_LEVELAgent model and log verbosity.
GIT_REMOTE_URL, GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_TOKENAgent git identity/remote; with GIT_REMOTE_URL empty the agent runs local-only (no clone/push).
NEXT_PUBLIC_AGENT_URLBrowser-side URL of the agent for Mission Control; defaults to http://localhost: + AGENT_PORT.
NEXT_PUBLIC_AGENT_WEBAPP_URLBrowser-side link target of the reader’s “Mission Control” rail entry; defaults to http://localhost: + AGENT_WEBAPP_PORT.
GH_CUSTOMER_ORG / GH_CUSTOMER_APP_ID / GH_CUSTOMER_INSTALLATION_ID and GH_INTERNAL_ORG / GH_INTERNAL_APP_ID / GH_INTERNAL_INSTALLATION_IDAurora’s two GitHub Apps (customer solutions org + internal org). Aurora starts fine with these blank — it shows a “Connect Aurora to GitHub” empty state.
GH_CUSTOMER_PRIVATE_KEY_PATH / GH_INTERNAL_PRIVATE_KEY_PATH (in-container paths under /secrets), GH_CUSTOMER_PRIVATE_KEY / GH_INTERNAL_PRIVATE_KEY (inline alternative), AURORA_SECRETS_PATHWhere Aurora finds the App private keys.
GH_PROVISION_TOKENCross-org fork credential (classic PAT) for solution creation.
AURORA_AGENT_ENABLED, AURORA_ANTHROPIC_API_KEY or AURORA_CLAUDE_CODE_OAUTH_TOKEN, AURORA_AGENT_MODEL, AURORA_AGENT_CWDAurora’s in-product agent chat (off by default).
AURORA_TENANTS_ENABLED, POSTGRES_PASSWORD, SERVICE_SHARED_SECRETPortfolio registry: tenants UI flag, local Postgres password (default orbit-local), and a dormant service-to-service guard (empty = off).
ORBIT_DEPLOY_ENABLED + AZURE_SUBSCRIPTION_ID, AZURE_RESOURCE_GROUP, AZURE_ACR_NAME, AZURE_ACA_ENV, AZURE_STORAGE_ACCOUNT, AZURE_LOCATION, AZURE_MANAGED_IDENTITY_RESOURCE_ID, AURORA_IMAGE_TAG, ORBIT_WEBAPP_IMAGE_TAG, ORBIT_AGENT_IMAGE_TAGOptional Azure auto-deploy on solution creation (off by default; non-secret coordinates only — locally your az login session is used).

Aurora’s Claude credential is deliberately AURORA_-prefixed. Compose ranks host-shell variables above .env, and host shells often export their own ANTHROPIC_API_KEY (frequently a subscription sk-ant-oat… token, which is invalid as an API key). Reading the credential from AURORA_ANTHROPIC_API_KEY / AURORA_CLAUDE_CODE_OAUTH_TOKEN prevents that shadowing. The orbit-agent’s ANTHROPIC_API_KEY is not protected this way — keep your shell clean or set the value in .env explicitly.

Day-to-day workflows

All commands run from infrastructure/local/ (or add -f infrastructure/local/docker-compose.yml from the repo root). The compose project is named orbit-local.

# Bring the whole stack up (build images if needed) docker compose up --build # foreground docker compose up --build -d # detached # Bring it down (containers + network; named volumes survive) docker compose down # Bring it down AND wipe named volumes (Postgres data, Claude sessions!) docker compose down -v # Restart one service (no rebuild, keeps anonymous volumes) docker compose restart orbit-webapp # Tail logs docker compose logs -f orbit-agent # one service docker compose logs -f --tail=100 # everything # Status docker compose ps # Shell into a container docker compose exec orbit-agent sh

Rebuilding after a dependency or shared-package change. Because node_modules lives in the image (the bind mount only carries source), any change to a package.json/lockfile — and any change to shared/trajectory-loader or shared/registry-kit, which are baked in at build time — needs an image rebuild:

docker compose up --build -d orbit-webapp # rebuild + recreate one service docker compose up --build -d # or the whole stack

Renewing anonymous volumes. restart does not recreate anonymous volumes. To force fresh node_modules (re-seeded from the image) and an empty .next:

docker compose up -d --force-recreate -V orbit-webapp

Known gotchas

globals.css edits don’t always hot-reload

Edits to orbit-webapp’s src/app/globals.css sometimes never trigger a Turbopack recompile, so the browser keeps serving the previously-compiled CSS even though the file inside the container is correct. Two things compound:

  1. macOS → Docker bind-mount file watching is unreliable — inotify events across the virtiofs/gRPC-FUSE bridge are frequently dropped, so Turbopack never sees the change (large-CSS edits drop more often than JS/TSX edits).
  2. .next is an anonymous volume, and docker compose restart does not recreate it — a plain restart reuses the stale compiled CSS.

The fix — clear .next inside the container, then restart:

docker compose exec -T orbit-webapp sh -c "rm -rf /app/orbit-webapp/codebase/.next/* /app/orbit-webapp/codebase/.next/.* 2>/dev/null" docker compose restart orbit-webapp # wait for "Ready in" in the logs, then hard-reload the browser # (the first request recompiles and is slow)

Equivalent: docker compose up -d --force-recreate -V orbit-webapp. A full down -v + up --build also fixes it, but wipes the Claude session volume.

When verifying a CSS change, don’t trust a single browser reload — confirm the rule actually shipped (e.g. via getComputedStyle) and clear .next if it’s stale, rather than assuming the edit was wrong.

Out-of-band solution-repo edits don’t show up in the reader

The agent holds the solution tree in memory and serves it to orbit-webapp over HTTP; there is no filesystem watcher on /repo. Edits the agent makes itself are picked up (and pushed to the reader via the solution.updated SSE event), but changes made out-of-band — editing the solution repo directly on the host — are invisible until you ask the agent to reload:

curl -X POST http://localhost:3010/solution/reload

(3010 is the default AGENT_PORT.)

Other things to know

  • down -v deletes orbit-claude-sessions — interrupted agent tasks can no longer be resumed afterwards. Prefer plain down, or the targeted --force-recreate -V <service> for volume problems.
  • The agent container runs as a sandbox trust boundary: compose sets IS_SANDBOX=1 so the Claude Code SDK’s bypassPermissions mode works under root inside the container.
  • SOLUTION_REPOSITORY_PATH is mandatory — compose fails fast with a clear error (:?Set SOLUTION_REPOSITORY_PATH in .env ...) if it’s unset.