Customer hosting

Deploy Grunt services behind one HTTPS hostname with your Kubernetes ingress

Use one HTTPS hostname for the Web Editor, AuthServer, Public API and MCP, with separate application pods and internal Kubernetes Services. This is the primary Kubernetes layout described below. You can supply your own Helm chart and use your cluster's Istio ingress. Azure Container Apps uses a separate installer.

Coming soonIn development

Single-domain Kubernetes deployment

The single-domain settings require an application release supplied by Grunt that includes configurable application, editor API and runtime paths.

#Select the application release

Ask Grunt for a matching, digest-pinned application release with Hosting:PathBase, Hosting:ApiPath and GruntRuntime:Path support before using this layout. Older images do not gain this support from an ingress rewrite. Do not mix individual service images from different releases. Use the images, startup commands, business settings and service credentials from that release's Compose manifest as your starting point; replace its loopback addresses and network namespace sharing with the Kubernetes configuration below.

You need Linux AMD64 nodes, persistent PostgreSQL and blob storage, registry pull credentials, a customer Entra app registration, deployment credentials, and a hostname reachable from users' browsers and MCP clients. The same public authority hostname must also resolve and be reachable from application pods for discovery and token operations. Split DNS is possible, but retain the same HTTPS issuer URL and certificate identity on both sides.

This guide describes configuration and routing. Acceptance on your cluster still requires real Entra sign-in, activation, MCP client OAuth/streaming and content persistence checks. Application support and a successful manifest render do not establish a tested cluster deployment or an HA guarantee.

#Single-domain route map

The examples use https://grunt.example.com. Replace it with your installation's hostname in every setting and your Entra registration.

External pathDestinationBackend port and handling
/auth-api and /auth-api/...AuthServerHTTP 8080; preserve the prefix; Hosting__PathBase=/auth-api.
/api and /api/...Public APIHTTP 8083; preserve the prefix; Hosting__PathBase=/api.
/editor/api and /editor/api/...Editor APIHTTP 8082; preserve the path; Hosting__ApiPath=/editor/api.
/mcp and /mcp/...MCPHTTP 8081; preserve the path; /mcp is the protocol endpoint, not a mount prefix to strip.
/runtime/...MCP runtime filesHTTP 8081; preserve the path; GruntRuntime__Path=/runtime.
/.well-known/oauth-protected-resource and its path suffixesMCP discoveryHTTP 8081; preserve the path, including /.well-known/oauth-protected-resource/mcp.
/auth/callback and /auth/logout-callbackEditorHTTP 8082; existing editor callback paths, distinct from AuthServer's Entra callback.
/ and all remaining pathsEditorHTTP 8082; frontend, static assets, /runtime-config.js, and SPA fallback. Place this route last.

Core (HTTP 8085), Web Service Layer (HTTP 8084), PostgreSQL and blob storage stay internal for the core editor/MCP flow. A migration job needs no Service, Ingress or HTTPRoute. Public API template operations can return signed blob URLs; clients using those operations also need access to the blob endpoint with the appropriate TLS and browser CORS configuration.

The Istio ingress example contains a Gateway and VirtualService for Services named auth, editor, public-api and mcp. Change the hostname, ingress selector, TLS credential name and Service names for your cluster. It does not install those Services, workloads or Istio. It also routes the standardized authorization-server discovery alias to AuthServer's prefixed discovery endpoint.

Use path-segment boundaries: an Istio string-prefix rule for /runtime would also match /runtime-config.js. The example matches /runtime/ and handles exact /runtime separately. It preserves streaming on /mcp, disables route retries there, and keeps the Editor catch-all last. It does not rewrite AuthServer, Public API or Editor API paths.

#Configure the application containers

Keep ASPNETCORE_ENVIRONMENT=Production, DOTNET_ENVIRONMENT=Production, DeploymentMode=OnPremise, AppConfigEndpoint empty, and the release's existing authentication and deployment enforcement. Replace ASPNETCORE_URLS with http://0.0.0.0:<backend-port> in each service so its Kubernetes Service can reach it. A pod's 127.0.0.1 cannot reach a different application's pod.

Supply service credentials, signing keys, certificates and connection strings through your cluster's secret-management integration, not Helm values committed to source. Preserve them across restarts and replicas. The release initializer can generate the same installation keys used by Compose; its printed four-port URLs are for Compose and must be replaced with this guide's addresses. Do not rerun it for an existing installation.

#Trust your ingress

Configure these settings on the four ingress-facing application hosts. The network below is an example only: use the actual proxy source IPs or narrowly scoped proxy network your applications observe, and enforce the matching ingress restrictions with NetworkPolicies or mesh authorization.

ReverseProxy__KnownNetworks__0: "10.42.0.0/24"
ReverseProxy__AllowedHosts__0: "grunt.example.com"
ReverseProxy__ForwardLimit: "1"

For fixed proxy addresses, use indexed ReverseProxy__KnownProxies__0 settings instead of a network. Loopback proxies remain trusted for the existing Nginx topology. Set ForwardLimit to your actual trusted forwarding chain, not an arbitrary high value. Your ingress must preserve the public Host and supply the original HTTPS scheme; only trusted proxies may affect those values. TLS certificates must be trusted by browsers, MCP clients and application containers. Do not disable transport-security or certificate validation to accommodate the proxy.

Hosting__PathBase consumes a prefix preserved in the request path; do not strip it at ingress. The application deliberately does not obtain its base path from a caller-supplied X-Forwarded-Prefix header. Paths start with /, have no trailing slash, and contain no escaped or dot segments.

#AuthServer

In addition to the release's Entra, database, signing/encryption certificate and persistent Data Protection settings, configure:

Hosting__PathBase: /auth-api
Auth__Issuer: https://grunt.example.com/auth-api/
Auth__McpResource: https://grunt.example.com/mcp
Auth__PublicApiResource: https://grunt.example.com/api
Auth__WebEditorOrigins__0: https://grunt.example.com

The issuer ends in / and its path must match Hosting__PathBase. The editor origin has no path or trailing slash. Register this exact Web redirect URI in your customer Entra app:

https://grunt.example.com/auth-api/signin-entra

Enable the ID-token email optional claim, require enterprise-application assignment and assign the allowed users/groups. The browser follows this callback, so a cluster-only DNS name is insufficient. A corporate hostname and private ingress accessible through your network or VPN can work; public Internet ingress is not inherently required. MCP clients hosted outside that network also need connectivity to the advertised MCP and authority endpoints.

#Public API

Hosting__PathBase: /api
GruntAuth__Authority: https://grunt.example.com/auth-api/
GruntAuth__PublicApiResource: https://grunt.example.com/api
GruntAuth__AcceptLegacyAuth0: "false"
WebEditorTokens__Issuer: https://grunt.example.com/api
CloudUrls__ClientPortalBaseUrl: https://grunt.example.com
CloudUrls__WebsiteBaseUrl: https://grunt.example.com
CloudUrls__DownloadBaseUrl: https://grunt.example.com/api

Retain the release's Public API signing keys, local storage settings, disabled legacy/hosted integrations and WSL credentials. For separate pods, set ServiceClientEndpoints__WebServiceLayer and AssetLinks__WebServiceLayerUrl to http://wsl:8084/, with their matching API keys.

#Web Editor and its API

Hosting__ApiPath: /editor/api
GruntAuth__Authority: https://grunt.example.com/auth-api/
GruntAuth__PublicApiResource: https://grunt.example.com/api
GruntAuth__AcceptLegacyAuth0: "false"
WebClient__VITE_AUTH_PROVIDER: grunt
WebClient__VITE_GRUNT_AUTHORITY: https://grunt.example.com/auth-api/
WebClient__VITE_GRUNT_RESOURCE: https://grunt.example.com/api
WebClient__VITE_API_BASE_URL: /editor/api
WebClient__VITE_PUBLIC_API_BASE_URL: https://grunt.example.com/api
WebClient__VITE_GRUNT_RUNTIME_URL: https://grunt.example.com/runtime

The frontend remains at /; /editor is its editing page, not the application's mount prefix. The editor backend and runtime assistant both use /editor/api. The editor's login/logout callbacks remain /auth/callback and /auth/logout-callback; keep both routed to the Editor. Set its WebServiceLayerUri and WebServiceLayerApiKey for WSL and GruntApiUri for Public API. Preserve the configured Public API resource identifier even when using an internal Service address for an application-to-application connection.

#MCP and runtime files

GruntAuth__Authority: https://grunt.example.com/auth-api/
GruntAuth__McpResource: https://grunt.example.com/mcp
GruntAuth__PublicApiResource: https://grunt.example.com/api
GruntAuth__AcceptLegacyAuth0: "false"
GruntRuntime__Path: /runtime
GruntRuntime__BaseUrl: https://grunt.example.com/runtime
WidgetEditorRuntimeDirectory: /app/Editor
WebAppPublicUrl: https://grunt.example.com
McpServerWidgetDomain: https://grunt.example.com

Set WebServiceLayerUri, WebServiceLayerApiKey and GruntApiUri for their respective backend connections. Keep runtime assets and discovery metadata reachable as well as /mcp; routing protocol traffic alone does not cover initialization and editor loading. Configure GruntApiUri with a trailing slash when it includes a prefix, for example http://public-api:8083/api/.

#Internal services and storage

Configure WSL's CoreWebServiceUri=http://core:8085/, WebServiceLayerApiKey and CoreWebServiceApiKey as in the release, and set AssetLink__PublicBaseUrl=https://grunt.example.com/api and AssetLink__EditorBaseUrl=https://grunt.example.com. On-prem mode permits the Public API application path in generated source/delivery URLs; the editor setting remains a root origin. Editor, MCP and Public API call WSL; WSL calls Core. Kubernetes DNS is sufficient for these internal connections. Internal request paths remain protected by the existing API keys and bearer/capability policies; mesh mTLS does not replace them.

Replace Compose database/blob hostnames with your persistent Kubernetes or managed storage endpoints in every applicable connection string. Share one logical installation database and blob store across application replicas. AuthServer replicas share the same signing/encryption certificates and persistent Data Protection key ring; Public API replicas share their signing keys. Do not create independent databases, blob stores or installation identities per replica, or scale PostgreSQL/Azurite by simply raising a replica count. Plan database/storage HA and migrations separately and test the intended application replica counts.

#Migrations, installation and verification

Run the Cloud migration image with DeploymentMode=OnPremise, then run dotnet Grunt.AuthServer.dll --migrate using AuthServer's configuration and keys before admitting application traffic. AuthServer also checks its schema at startup. Migrations must finish successfully; they do not serve HTTP and need no public route. A Helm pre-install job can only use a database, Secrets and network access that already exist: ordinary resources in the same chart are created after that hook. Choose your chart's dependency and startup ordering accordingly.

Allow pod DNS and the documented service/database/blob connections. Permit outbound HTTPS to Microsoft identity endpoints, the configured Grunt activation/renewal/usage endpoints, and client-metadata endpoints needed by your MCP clients. Registry pulls also need network access from Kubernetes nodes. Apply your existing Istio authorization, mTLS and egress policies; this layout does not require disabling mesh injection or bypassing the proxy.

Before rollout, verify:

  1. Auth discovery at /auth-api/.well-known/openid-configuration advertises the exact issuer and prefixed endpoints. MCP discovery at /.well-known/oauth-protected-resource/mcp advertises the exact /mcp resource and authority.
  2. An assigned Entra user can sign in, return to the Editor, renew the session and sign out. Check the actual callback registration and certificate trust, not just discovery.
  3. /editor/api/ping reaches the Editor API, protected Editor/API requests reject missing or invalid credentials, and /api never consumes the Editor's requests.
  4. /runtime/runtime.js and its _framework files load with appropriate MIME types; /runtime-config.js still reaches the Editor.
  5. A real OAuth-capable MCP client can initialize, list tools and call a tool through the ingress with streaming enabled.
  6. Create, edit, save, export, reopen and follow a generated asset link. Confirm deployment activation and usage acknowledgement, then repeat persistence and sign-in checks after restart and across the configured replicas.

Back up PostgreSQL, blobs, signing/session keys and installation credentials together. An image rollback does not undo a schema migration. Never reinitialize retained data or use new installation credentials to repair a routing issue.

#Packaged single-pod chart alternative

The existing packaged chart is an alternative to the separate-service layout above. It still uses one application pod, the Nginx gateway and four HTTPS ports. The settings above are for your own service-based deployment; they do not change that chart into separate pods or single-domain ingress. Keep its existing values, Secrets and PVC identities when upgrading it.

#Get a preview release

Ask Grunt for the registry hostname, exact chart version and registry pull credentials before following this guide. The Compose and ACA downloads currently linked in the setup guides do not contain this chart. Do not substitute their image release identifiers for a Helm chart version.

Your registry credentials must allow pulling both on-premise/charts/grunt and the application's image repositories. Registry access, your Grunt deployment credentials and Microsoft Entra sign-in have separate purposes.

The preview has passed chart packaging, manifest schema and Istio injector checks. A live deployment on your cluster still needs acceptance testing before you use it for customer workloads.

#Before you start

You need:

  • Linux AMD64 Kubernetes nodes and a Helm version compatible with your cluster. All application containers run together, so one node must have capacity for the complete application pod.
  • kubectl access to your installation namespace and permission to create StatefulSets, Services, Secrets, persistent volume claims and NetworkPolicies.
  • A dynamic default StorageClass, or storage classes configured under persistence in your chart values. The default claims request 20 GiB for PostgreSQL, 100 GiB for blobs and 1 GiB for authority session keys. Review these and the container resources for your workload.
  • A network plugin that enforces Kubernetes NetworkPolicies, or equivalent platform controls agreed with your cluster administrator.
  • A reachable DNS hostname and a TCP load balancer or TLS-passthrough routes for the four public HTTPS ports below.
  • A customer-tenant Entra app registration and secret, plus a Grunt deployment client and registry pull credentials.
  • Python 3.9+ and Docker on an administration machine for one-time key generation. An ARM64 administration machine needs Linux AMD64 container emulation.
  • Outbound connectivity for registry pulls, Microsoft sign-in and Grunt activation, entitlement renewal and aggregate usage reporting.

The chart retains one application pod containing AuthServer, MCP, Web Editor, Public API, Web Service Layer and Core, alongside the Nginx gateway. PostgreSQL and Azurite each run in a separate StatefulSet. This preview does not provide independent scaling of the application services or automatic failover.

Istio and ingress

Your namespace or control-plane revision determines whether Istio injects a proxy. The chart does not force injection, disable it or add traffic exemptions. Additional podAnnotations can supply your platform's proxy configuration.

For injected application pods, use Kubernetes 1.33+ and a compatible supported Istio release with native sidecar injection. The application pod requests sidecar.istio.io/nativeSidecar: "true", which starts an injected proxy before the database migration init container. It does not enable injection by itself. Legacy regular-container sidecars cannot provide this startup ordering. See Istio's native-sidecar setting.

Your mesh's mTLS, authorization and egress policies apply to traffic between pods; calls between application containers remain on loopback and retain their existing authentication. Test those policies against migration access to PostgreSQL, blob storage, sign-in and activation. Ambient-mode installations require separate validation.

The chart keeps Nginx as the HTTPS endpoint. Preserve its hostname, ports and existing paths when configuring your ingress. Arbitrary subpaths, independent per-service Istio routes and HTTPS termination at another proxy require additional integration work.

ServicePublic URL
Sign-in authorityhttps://<hostname>:17440/
MCP serverhttps://<hostname>:17441/mcp
Web Editorhttps://<hostname>:17442/editor
Public APIhttps://<hostname>:17443/

Keep PostgreSQL, blob storage, Web Service Layer and Core internal.

#1. Download the chart

Use a private folder on the administration machine's persistent disk for each installation. Keep its configuration and keys for restarts and upgrades. The following commands start a new installation folder; choose a different unused name for each installation.

mkdir -m 700 grunt-helm-installation && cd grunt-helm-installation
export GRUNT_REGISTRY="<registry-hostname-from-Grunt>"
export GRUNT_CHART_VERSION="<exact-chart-version-from-Grunt>"
helm registry login "$GRUNT_REGISTRY" --username "<registry-username>"
helm pull "oci://$GRUNT_REGISTRY/on-premise/charts/grunt" \
  --version "$GRUNT_CHART_VERSION" --untar
cd grunt

Enter the registry password at the prompt. If Grunt supplied a .tgz chart and checksum instead, verify the checksum before extracting it. You do not need source code, Aspire, .NET or private build feeds.

#2. Configure a new installation

Authenticate Docker separately so the initializer can run the chart's pinned key-generation image:

docker login "$GRUNT_REGISTRY" --username "<registry-username>"
cp files/defaults.env .env.example
python3 files/init_env.py .

Enter your DNS hostname, Entra tenant/application settings and Grunt deployment credentials. The initializer creates .env and secrets/ with unique service credentials, signing/encryption keys, a trust bundle and a TLS certificate. It refuses to overwrite an existing installation.

To use an organization-issued certificate, pass its PEM chain and private key to the initializer instead:

python3 files/init_env.py . --certificate chain.pem --key key.pem

Trust the installation certificate on clients if you use the generated certificate. Keep certificate verification enabled.

Enable Microsoft sign-in

In your Entra app registration, add the Web redirect URI printed by setup, using the same hostname as your chart values:

https://<hostname>:17440/signin-entra

Enable the email optional claim for ID tokens. Require assignment in the enterprise application and assign the allowed users or groups. Grunt accepts identities from the configured tenant and creates local users on first successful sign-in. An Entra email claim alone does not establish verified email for operations that require it.

Create the Kubernetes Secrets

Create these Secrets in the installation namespace, or use your platform's secret-management integration to provide the same keys. Customize their names under secrets in your chart values if needed. Keep secret values out of Helm values.

kubectl create namespace grunt
umask 077
# kubectl retains dotenv quotes; remove the initializer's quoting before importing.
sed "s/='\(.*\)'$/=\1/" .env > installation.env
kubectl -n grunt create secret generic grunt-installation --from-env-file=installation.env
rm installation.env
kubectl -n grunt create secret generic grunt-tls --from-file=secrets/tls
kubectl -n grunt create secret generic grunt-trust --from-file=secrets/trust
kubectl -n grunt create secret generic grunt-auth --from-file=secrets/auth
kubectl -n grunt create secret generic grunt-editor --from-file=secrets/public-api

Also provide an image-pull Secret named grunt-registry in this namespace through your platform's credential-management process, or set imagePullSecrets to an existing pull Secret. Helm registry login does not authenticate Kubernetes image pulls.

The configuration Secret needs POSTGRES_PASSWORD, CORE_API_KEY, WSL_API_KEY, JWT_SIGNING_KEY, ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET, DEPLOYMENT_CLIENT_ID and DEPLOYMENT_CLIENT_SECRET. The initializer supplies them. Each container references only the parameters it uses; deployment credentials remain scoped to Web Service Layer.

#3. Install Grunt

Create an installation-values.yaml file for your installation, leaving the chart's default values.yaml intact. Use the same hostname you entered during initialization:

publicHost: grunt.example.com
service:
  type: LoadBalancer

The default Service is ClusterIP. Choose LoadBalancer for a TCP load balancer, or retain ClusterIP and configure your platform's TLS-passthrough routes. Point DNS at the resulting endpoint. Review the packaged chart defaults with helm show values . before choosing storage classes, sizes or container resources.

helm upgrade --install grunt "oci://$GRUNT_REGISTRY/on-premise/charts/grunt" \
  --version "$GRUNT_CHART_VERSION" --namespace grunt --values installation-values.yaml \
  --wait --timeout 4h
kubectl -n grunt get pods,pvc,svc

Select the exact supplied chart version. Migrations run before the applications start, including on upgrades and pod replacement. A failed migration prevents application startup. The timeout allows for schema migrations; it does not replace inspecting a stalled workload.

#4. Verify the installation

For the release name and namespace used above, check activation in the Web Service Layer container:

kubectl -n grunt exec grunt-grunt-0 -c wsl -- sh -c \
  'curl -fsS -H "X-Api-Key: $WebServiceLayerApiKey" http://127.0.0.1:8084/deployment/status'

Require enabled: true and status: "active". A ready pod alone does not confirm activation or working sign-in.

Complete these checks with your platform's networking and security policies enabled:

  1. Open the Web Editor, sign in with an assigned Microsoft account, and create, save, export and reopen a chart.
  2. Connect an OAuth-capable MCP client to the MCP endpoint. Complete registration, sign-in and consent, then initialize, list tools and call a tool. Check streaming responses.
  3. Confirm usage acknowledgement advances after saved changes and pending reporting clears.
  4. Restart the application StatefulSet and repeat sign-in, reopen and export. Confirm activation and stored content persist.
  5. Test a consistent backup restore before relying on the installation for retained content.

If Istio is injected, inspect kubectl -n grunt get pod grunt-grunt-0 -o yaml. The restartable istio-proxy init container must precede migrations, and its startup probe must succeed before migrations run.

#Upgrade and preserve data

Back up PostgreSQL, blob storage, authority session keys and installation Secrets together at a consistent point. Preserve the Helm release name, namespace, values, Secrets and PVCs. Do not rerun initialization or generate replacement keys for an existing installation.

If your Helm release name has 40 or more characters, set fullnameOverride in your installation values to the existing application StatefulSet name before upgrading. List the release's StatefulSets with kubectl -n <namespace> get statefulsets -l app.kubernetes.io/instance=<release>: their names are <prefix>, <prefix>-postgres and <prefix>-storage. Use that exact prefix and keep it unchanged on every upgrade. This reuses the original PostgreSQL, blob and auth-key claims, whether your installation used truncated or hashed resource names. Helm upgrades with a long release name stop if this value is missing. For a new long-name installation, you can choose a fixed, unique fullnameOverride at first install. Pin the existing name before updating through GitOps too; a GitOps renderer may not mark its operation as a Helm upgrade.

Obtain the next approved chart version, arrange backups and a maintenance window, update GRUNT_CHART_VERSION, and rerun the install/upgrade command with the same values. Then repeat the acceptance checks above. The chart pins application image digests; changing image tags independently does not select a complete release.

Helm rollback does not undo database migrations. StatefulSet-created PVCs are retained by default on uninstall; deleting a PVC can delete its storage according to your StorageClass. Plan storage-class or claim-size changes separately. Updating an environment-backed Secret requires restarting the affected workload; changing the PostgreSQL Secret alone does not rotate the database password. Keep .env, secrets/ and recovery material private and backed up.

#Troubleshooting

SymptomCheck
Chart pull deniedConfirm the supplied registry, chart version and pull credentials. Access to application images alone does not grant chart access.
Pod reports ImagePullBackOffCheck the namespace's image-pull Secret and its access to the application's image repositories. Helm login applies only to chart downloads.
Migration remains pending with IstioInspect the pod's init containers and events. Require native proxy startup before migrations, working control-plane connectivity and permitted database traffic.
PVC remains pendingCheck the configured/default StorageClass, capacity and volume topology against the pod's node requirements.
Sign-in or activation failsCheck the exact Entra callback, user assignment, deployment credentials, certificate trust, DNS and outbound access.

For help, provide the chart version, its application release, approximate UTC time and redacted error messages. Never send .env, installation Secrets, passwords, tokens or private keys.