AppHost (MILTON.AppHost)
This project wraps the whole application together utilizing .NET Aspire. It acts as the orchestrator to spin up all services and infrastructure:
Resources
flowchart TD subgraph Infrastructure PG["PostgreSQL"] RMQ["RabbitMQ"] RustFS["RustFS (S3)"] Gotenberg["Gotenberg (PDF)"] KC["Keycloak (Auth)"] end subgraph Services API["MILTON API"] NS["NotificationService"] DG["DocumentGenerator"] Git["GitOperations"] Mig["MigrationService"] Cluster["Clustering (Python)"] end Gateway["YARP Gateway"] Client["MILTON.Client (Blazor WASM)"] Gateway -- "/keycloak/**" --> KC Gateway -- "/hubs/**" --> NS Gateway -- "/milton-documents/**" --> RustFS Gateway -- "/**" --> API API --> PG API --> RMQ API --> Gateway API --> KC API --> Gotenberg API --> Cluster API --> NS NS --> RMQ DG --> PG DG --> RMQ Git --> RMQ Mig --> PG Client --> Gateway Client --> KC
| Resource | Type | Purpose |
|---|---|---|
| PostgreSQL | Container | Primary database for all services |
| RabbitMQ | Container | Message broker for Wolverine messaging |
| RustFS | Container | S3-compatible object storage for documents |
| Gotenberg | Container | Stateless PDF conversion sidecar |
| Keycloak | Container | OpenID Connect identity provider |
| YARP Gateway | ASP.NET Core | Reverse proxy — unified TLS entry point routing to API, NotificationService, RustFS, and Keycloak |
| MILTON API | ASP.NET Core | Primary REST API + Blazor host |
| NotificationService | ASP.NET Core | Standalone SignalR hub + Wolverine RabbitMQ consumer for real-time notifications |
| DocumentGenerator | Worker Service | Wolverine saga for document processing |
| GitOperations | Worker Service | Repository cloning via RabbitMQ commands |
| MigrationService | Worker Service | One-shot EF Core database migrations |
| Clustering | Docker (Python) | Code analysis using Leiden algorithm |
| MILTON.Client | Blazor WASM | Frontend SPA |
Running
Run aspire run in the project root to compose the whole system together.
Deployment (Kubernetes / GHCR)
The AppHost also defines a Kubernetes compute environment (k8s) used only during
aspire publish / aspire deploy (never in local aspire run mode):
-
Container registry — images are pushed to a private GHCR repository and pulled from there by the cluster. The registry endpoint and repository are parameterized:
registry-endpoint(defaultghcr.io) →Parameters__registry_endpointregistry-repository(defaultjanne022/milton) →Parameters__registry_repository
-
Helm namespace — the generated chart deploys into the
miltonnamespace. -
Image pull secret — every Kubernetes workload (API, gateway, notificationservice, gitoperations, documentgenerator, api-migrations, docgen-migrations, clustering, web) gets
spec.template.spec.imagePullSecrets: [{ name: "ghcr-pull" }]viaPublishAsKubernetesService, so the cluster can pull the private GHCR images. -
EKS ALB Ingress — the chart also contains an
Ingressresource (public) using IngressClassalb(AWS Load Balancer Controller). It terminates TLS at the edge and routes all traffic to the single YARP gateway, which internally proxies web, /api, Keycloak, MCP, and S3. The ingress hostname comes from thepublic-hostnameparameter:public-hostname(defaultmilton.example.com) →Parameters__public_hostname
-
TLS / certificates —
WithTls()makes Aspire generate a self-signed bootstrap TLS secret on first deploy. Production clusters should install cert-manager to replace it with a real certificate (for AWS ALB + ACM, the generated Ingress can alternatively be annotated withalb.ingress.kubernetes.io/certificate-arn). -
Keycloak / OIDC URLs (publish mode) — in publish mode the Keycloak
KC_HOSTNAMEand the gateway’sAuthentication__Keycloak__Authorityare full-URL parameters passed directly toWithEnvironment, so they are resolved at deploy time (rather than being derived frompublic-hostnamevia string interpolation, which would bake the AppHost default into the chart at publish time):keycloak-public-base-url(defaulthttps://milton.example.com/keycloak) →KC_HOSTNAME→Parameters__keycloak_public_base_urlkeycloak-public-authority(defaulthttps://milton.example.com/keycloak/realms/milton) →Authentication__Keycloak__Authority→Parameters__keycloak_public_authority
Dev mode keeps the
https://localhost:7190/...URLs unchanged.
Dev-only knowledge base (not deployed to production)
The knowledge-base subsystem — Neo4j (graph DB), Ollama (nomic-embed-text embeddings),
milton-knowledge-db, the knowledge migrations + seeding workers, and the mcp-server
(milton-knowledge MCP tooling used by AI agents during development) — is gated behind
!builder.ExecutionContext.IsPublishMode in the AppHost. It is intentionally not included
in the production Helm chart: aspire publish / aspire deploy do not emit deployments or
services for neo4j, ollama, knowledgebase-seeder, knowledge-migrations, mcp-server, or
milton-knowledge-db. The main MILTON API and every production service have zero code references
to these resources. As a result NEO4J_PASSWORD (Parameters__neo4j_password) is not required by
the production CD workflow. The postgres host-port binding, PgAdmin, and the pgvector
initialization bind mount are likewise dev-only.
Vault in production (no static root token)
Dev mode starts a local hashicorp/vault dev container and wires Vault__Address +
Vault__Token=root to the API and DocumentGenerator. In publish mode the dev container is
omitted and the services instead receive parameterized production auth:
vault-address(defaulthttps://vault:8200) →Vault__Addressvault-auth-method(defaultToken) →Vault__AuthMethod(Token | AppRole | Kubernetes)vault-role-id/vault-secret-id→Vault__RoleId/Vault__SecretId(AppRole)vault-kubernetes-role→Vault__KubernetesRole(Kubernetes)
No static Vault__Token is emitted in publish mode, and Vault__AllowInsecureHttp is left
unset (HTTPS enforced by MILTON.Platform’s VaultSecretClient).