Deployment
MeshWeaver has two distinct deploy routes. They target different infrastructure — pick the one that matches where you're deploying. Neither is deprecated.
| Route | Target | How | Doc |
|---|---|---|---|
| AKS | Shared cluster <aks-cluster> — every Deployments/<name> record (memex, memex-cloud, …) |
A Roll Hosting/InstanceAction on the control instance (the record's image pin); the operator runs build images → kubectl set image + rollout. Direct az aks command invoke is break-glass |
OperatingFromThePortal.md · DeploymentAKS.md |
| Azure Container Apps | .NET Aspire test / prod modes (ACA, Sweden Central) |
tools/deploy.sh prod\|test (wraps aspire deploy + migration-exit + db-version gate) |
DeploymentContainerApps.md |
🚨 The Deployment record is the ONE input; Aspire and Helm render from it; the image receives it as configuration (Deployment:Record); Aspire emits a record, never a chart. An instance is a DeploymentContent record (Deployments/<name> on the control instance), built fluently (builder.AddMemex("memex").WithImage(…).WithPluginRepo(…)…) or by hand, and every route reads that record — nothing is configured twice. Maintainer, 2026-09-08; ConfiguringAnInstanceFromAspire.md carries the fluent surface and the parity table.
Which doc do I need?
| Scenario | Read |
|---|---|
Declare an instance ONCE — the Deployment record, the fluent builder, how Aspire and Helm render from it and how the image receives it (Deployment:Record); the parity table method → field → Helm value → config key |
ConfiguringAnInstanceFromAspire.md |
Operate an instance without cluster access — roll, restart, suspend, audit, reconcile as Hosting/InstanceActions; which reads the API does not answer yet; how to read every kubectl recipe in this tree |
OperatingFromThePortal.md |
| See every running instance — who it's for, its infra, database, and version — and how to create or delete one | Instances.md |
| Know what each instance actually runs — platform build, commit, framework identity, update policy and every module's pinned coordinate, reported by the instance itself, hourly | DeploymentInventory.md |
| Database backups & disaster recovery — managed PITR, geo-redundancy, restore | DatabaseBackups.md |
| Understand the release model, merge gates, version channels, and policy-driven self-update | ReleaseStrategy.md |
| Know what CD guarantees about a published image set — all-or-nothing publication, the promote ordering, the self-healing reconciler — and why you verify the IMAGE and never the green tick | ContinuousDeliveryContract.md |
Work out whether an install can actually take the newest release — the schema boundary self-update cannot cross, why the resulting stall is invisible, and the three conditions a tag must clear before it is a safe helm upgrade target |
SelfUpdateSchemaWall.md |
Ship a code update to the memex portal on the shared AKS cluster |
DeploymentAKS.md |
Deploy an Aspire-orchestrated test/prod Container Apps environment |
DeploymentContainerApps.md |
| Understand the private-AKS-cluster architecture & operations behind the shared portal | MemexCloudDeployment.md |
| Add a new tenant environment on the existing shared AKS platform | OnboardingNewEnvironment.md |
| Run a prod-like memex locally on a Mac (Colima k3s, arm64) | LocalColimaMac.md |
Instance-specific configuration options (portal.example.com) |
DeploymentOptions.md |
| Reclaim space — delete old ACR images / prune local Docker, safely | ImageCleanup.md |
| Turn production errors into tickets automatically — deploy the red-log watcher, route incidents to repositories, or work out why nothing is being reported | LogWatchTriage.md |
The two routes provision and run on different platforms (raw AKS deployments + Helm vs. ACA via Aspire), with different update mechanics; they are not interchangeable. The sections below (local run, Azure AD, secrets, project layout) are shared across both routes.
How a release reaches the fleet
The contract (maintainer, 2026-09-03: "end of github pipeline must call memex, which must register release and publish event") is three sentences:
- Every publishing pipeline ENDS with one call to memex. Core's CD, after the image set is
promoted, POSTs the signed platform build (
event: platform-build) into the control instance'sHosting/PlatformBuildsinbox (notify-platform-update). Every node repository'snode-repo-publish-bake.ymlrun, after its bundles are sealed for an identity, POSTs the signed publication record (event: bundle-publication— source, identity, commit, tester + portal image) into the same inbox (register-publication, its last job). Nothing runs after that call, and no pipeline sends arepository_dispatchto another repository. - memex REGISTERS the release as a durable node —
Hosting/PlatformBuilds/<version>for a platform build,Hosting/Publications/<identity>/<source>for a bundle publication — the source of truth for "what is published for which identity" (what the self-update availability check reads). - memex PUBLISHES the event from that registration:
FrameworkReleaseBroadcastersendsmeshweaver-framework-released(platform) ormeshweaver-upstream-published(bundle publication,client_payload.version= the identity) to the subscribed repositories — the repositories the control instance'sHosting/Deploymentrecords name as registry sources. The subscribers' CI receives it, resolves both images from the version, builds and publishes for that identity — and ends by calling memex (1).
pipeline (core CD | a node repo's publish-bake) memex (control instance) subscriber CI
─────────────────────────────────────────────── ──────────────────────── ─────────────
promote / seal ✅ WebhookInbox Hosting/PlatformBuilds
└─ ONE signed POST ──(platform-build |──────────────▶│ verify HMAC
bundle-publication)… and FINISH ├─ REGISTER Hosting/PlatformBuilds/<version>
│ Hosting/Publications/<identity>/<source>
├─ subscribers = Hosting/Deployment records'
│ pluginRepos[].isRegistrySource
└─ PUBLISH repository_dispatch ─────────────▶ on: repository_dispatch:
meshweaver-framework-released | types: [meshweaver-framework-released,
meshweaver-upstream-published meshweaver-upstream-published]
→ bake for the version → seal → POST memex
Where the pieces are: the POST steps in main-cd.yml and node-repo-publish-bake.yml (this repo);
the inbox watcher, registration and broadcast in the Hosting module's PlatformBuildInboxWatcher
(MeshWeaver.Plugins, Hosting/Deployment/Source); the broadcaster in src/MeshWeaver.GitSync.
PlatformReleaseNotifyGuard.CoreDispatchesToNoRepository refuses a dispatch SENDER in any workflow
under .github/workflows — there is no ledger — and
UpstreamBuildGateGuard.TheLaneEndsByRegisteringWithMemex_AndDispatchesToNobody pins the lane's call.
Operator view: after a promote, the control instance's log carries [PlatformBuilds] verified build …,
then [PlatformBuilds] release broadcast for <version>: N subscriber(s) dispatched.; each subscribed
repository shows a repository_dispatch run whose payload carries source: memex; the node repos'
pin-bump PRs follow. A 2xx on the pipeline's POST proves only that memex STORED the record.
Running Locally
Aspire (local mode)
Full local development with Docker containers (PostgreSQL pgvector + Azurite, Orleans in-process):
aspire run --project ../MeshWeaver.Plugins/src/Memex.AppHost/Memex.AppHost.csproj -- --mode local
Monolith (standalone, no Docker)
Lighter setup without Orleans or external infrastructure:
dotnet run --project ../MeshWeaver.Plugins/src/Memex.Portal.Monolith
# or via the AppHost:
aspire run --project ../MeshWeaver.Plugins/src/Memex.AppHost/Memex.AppHost.csproj -- --mode monolith
Azure AD App Registration
Microsoft authentication requires an app registration in Microsoft Entra ID (Azure AD).
- Azure Portal → App registrations → select your app (or create one)
- Under Authentication → Platform configurations → Web, add redirect URIs:
https://localhost:7122/signin-microsoft(local Monolith — HTTP fallback port 5022)https://localhost:7202/signin-microsoft(local Aspire portal — HTTP fallback port 5202)https://<your-deployed-domain>/signin-microsoft(deployed environments)
- Note the Application (client) ID and Directory (tenant) ID from the Overview page
- Under Certificates & secrets, create a client secret
For single-tenant apps, configure the tenant ID explicitly — the default /common endpoint is not supported.
Secrets Management
Secrets are stored in dotnet user-secrets for local development and in GitHub secrets for CI/CD. (On AKS, secrets come from Key Vault through a SecretProviderClass the chart renders from the keyVaultSecrets values block — names only, never values; see DeploymentAKS → "Key Vault secrets are DECLARED in values".)
Parameters for distributed modes (the authoritative list is the builder.AddParameter(...) calls in ../MeshWeaver.Plugins/src/Memex.AppHost/Program.cs):
| Parameter | Description | If unset |
|---|---|---|
Parameters:azure-foundry-key |
Azure AI Foundry API key (LLM access) | Required |
Parameters:azure-foundry-endpoint |
Azure AI Foundry /models endpoint |
Optional — defaulted in appsettings.json |
Parameters:anthropic-endpoint |
Anthropic-compatible endpoint | Required — blank yields Endpoint is missing for model 'X' |
Parameters:anthropic-model-0/1/2 |
Model catalog offered in the composer's model picker | Required — blank yields an empty model dropdown |
Parameters:embedding-endpoint |
Embedding model endpoint | Optional (defaults to empty) |
Parameters:embedding-key |
Embedding model API key | Optional (defaults to empty) |
Parameters:embedding-model |
Embedding model name | Optional (defaults to empty) |
Parameters:key-protection-master-key |
Encrypts ModelProvider API keys at rest |
Falls back to a dev default that is not secret — test/prod MUST override |
Parameters:microsoft-client-id |
Microsoft OAuth client ID | Required |
Parameters:microsoft-client-secret |
Microsoft OAuth client secret | Required |
Parameters:microsoft-tenant-id |
Microsoft Entra tenant ID (single-tenant apps) | Optional — omitted when empty |
Parameters:google-client-id |
Google OAuth client ID | Optional (defaults to empty) |
Parameters:google-client-secret |
Google OAuth client secret | Optional — omitted when empty |
Parameters:linkedin-client-secret |
LinkedIn publishing (client id is inlined in the AppHost) | Optional |
Parameters:custom-domain |
Custom domain for the deployed portal | Optional — omitted when empty |
Parameters:certificate-name |
TLS certificate name for the custom domain | Optional — omitted when empty |
🚨 Several of these deliberately carry no
value:default in the AppHost. That is not an oversight: passingvalue: ""makes Aspire resolve the parameter to the empty string and skip the user-secrets/config lookup entirely, so the setting silently stays blank even when user-secrets has it. Don't "tidy" a default onto them.
Set a secret with:
cd ../MeshWeaver.Plugins/src/Memex.AppHost
dotnet user-secrets set "Parameters:azure-foundry-key" "<your-key>"
Project Structure
memex/aspire/
├── Memex.AppHost/ # Aspire orchestrator — defines all resources
├── Memex.Aspire.Hosting/ # The Aspire adapter (builder.AddMemex) — derives everything from the Deployment record
├── Memex.Portal.Distributed/ # Portal with co-hosted Orleans silo
├── Memex.Portal.ServiceDefaults/ # Shared service defaults (health, telemetry)
└── Memex.Database.Migration/ # Database migration project (runs MigrationRegistry.All)
Outside aspire/, memex/ also holds Memex.Portal.Monolith (the standalone dev portal), Memex.Portal.Shared (shared portal code, including the self-update poller), Memex.Client, and Memex.LocalMesh.