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
ResourceTypePurpose
PostgreSQLContainerPrimary database for all services
RabbitMQContainerMessage broker for Wolverine messaging
RustFSContainerS3-compatible object storage for documents
GotenbergContainerStateless PDF conversion sidecar
KeycloakContainerOpenID Connect identity provider
YARP GatewayASP.NET CoreReverse proxy — unified TLS entry point routing to API, NotificationService, RustFS, and Keycloak
MILTON APIASP.NET CorePrimary REST API + Blazor host
NotificationServiceASP.NET CoreStandalone SignalR hub + Wolverine RabbitMQ consumer for real-time notifications
DocumentGeneratorWorker ServiceWolverine saga for document processing
GitOperationsWorker ServiceRepository cloning via RabbitMQ commands
MigrationServiceWorker ServiceOne-shot EF Core database migrations
ClusteringDocker (Python)Code analysis using Leiden algorithm
MILTON.ClientBlazor WASMFrontend 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 (default ghcr.io) → Parameters__registry_endpoint
    • registry-repository (default janne022/milton) → Parameters__registry_repository
  • Helm namespace — the generated chart deploys into the milton namespace.

  • 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" }] via PublishAsKubernetesService, so the cluster can pull the private GHCR images.

  • EKS ALB Ingress — the chart also contains an Ingress resource (public) using IngressClass alb (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 the public-hostname parameter:

    • public-hostname (default milton.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 with alb.ingress.kubernetes.io/certificate-arn).

  • Keycloak / OIDC URLs (publish mode) — in publish mode the Keycloak KC_HOSTNAME and the gateway’s Authentication__Keycloak__Authority are full-URL parameters passed directly to WithEnvironment, so they are resolved at deploy time (rather than being derived from public-hostname via string interpolation, which would bake the AppHost default into the chart at publish time):

    • keycloak-public-base-url (default https://milton.example.com/keycloak) → KC_HOSTNAME → Parameters__keycloak_public_base_url
    • keycloak-public-authority (default https://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 (default https://vault:8200) → Vault__Address
  • vault-auth-method (default Token) → 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).

0 items under this folder.