untested Untested configuration

This configuration is provided as a starting point and reviewed against the Apache setup that Aetheus actually runs. It has not been executed on a real deployment, so treat every value as something to verify rather than as a proven recipe.

IIS

An IIS layout transposed from the Apache configuration Aetheus runs in production. Same two host names, same two rules on the frontend, same WebSocket requirement on the API.

Components to install

  • URL Rewrite and Application Request Routing, if you proxy to containers or to a separately hosted backend.
  • WebSocket Protocol, a Windows feature under Web Server, Application Development. Without it the API host cannot upgrade connections.
  • .NET Core Hosting Bundle, only if you host the backend as an IIS application rather than proxying to it.

After installing ARR, enable proxying once at server level: IIS Manager, server node, Application Request Routing Cache, Server Proxy Settings, tick Enable proxy. Nothing below works until that box is ticked.

Two possible layouts

LayoutBackendFrontend
Proxy Runs on its own, IIS forwards to 127.0.0.1:10026. IIS forwards to 127.0.0.1:10025, or serves the files.
Hosted An IIS application using the ASP.NET Core Module. IIS starts, recycles and logs it. A static IIS site pointing at the published wwwroot.

The hosted layout is usually the better fit on a Windows host: it avoids running a second service manager alongside IIS.

App site: serving the frontend

Create a site bound to app.example.com with your certificate, pointing at the published wwwroot. Then place this web.config in that folder:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>

    <staticContent>
      <!-- The .NET WebAssembly runtime is served from these; IIS does not know them by default
           and answers 404, which looks like a broken build rather than a MIME problem. -->
      <remove fileExtension=".wasm" />
      <mimeMap fileExtension=".wasm" mimeType="application/wasm" />
      <remove fileExtension=".dat" />
      <mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
      <remove fileExtension=".blat" />
      <mimeMap fileExtension=".blat" mimeType="application/octet-stream" />
      <remove fileExtension=".pdb" />
      <mimeMap fileExtension=".pdb" mimeType="application/octet-stream" />
      <remove fileExtension=".br" />
      <mimeMap fileExtension=".br" mimeType="application/octet-stream" />
    </staticContent>

    <rewrite>
      <rules>
        <!-- Client-side routing: anything that is not a real file falls back to index.html. -->
        <rule name="SPA fallback" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
          </conditions>
          <action type="Rewrite" url="/index.html" />
        </rule>
      </rules>

      <outboundRules>
        <!-- Never cache the boot files, or a browser keeps the previous version after an upgrade. -->
        <rule name="No store on boot files">
          <match serverVariable="RESPONSE_Cache-Control" pattern=".*" />
          <conditions>
            <add input="{REQUEST_URI}" pattern="^/(index\.html|service-worker(\.published)?\.js|_framework/|_content/Radzen\.Blazor/)" />
          </conditions>
          <action type="Rewrite" value="no-cache, no-store, must-revalidate" />
        </rule>
      </outboundRules>
    </rewrite>

    <httpProtocol>
      <customHeaders>
        <add name="X-Content-Type-Options" value="nosniff" />
        <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
      </customHeaders>
    </httpProtocol>

  </system.webServer>
</configuration>

After publishing, remember to restore wwwroot/appsettings.json with your ApiBaseUrl; see the binaries page.

API site: the backend

Hosted with the ASP.NET Core Module

Create a site bound to api.example.com, pointing at the published backend folder, with an application pool set to No Managed Code. Then:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
    </handlers>
    <aspNetCore processPath="dotnet"
                arguments=".\Aetheus.Back.dll"
                stdoutLogEnabled="false"
                hostingModel="inprocess">
      <environmentVariables>
        <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
        <environmentVariable name="Aetheus__PublicApiBaseUrl" value="https://api.example.com" />
        <environmentVariable name="Cors__Origins__0" value="https://app.example.com" />
      </environmentVariables>
    </aspNetCore>
    <webSocket enabled="true" />
  </system.webServer>
</configuration>

Do not put secrets in web.config

The connection string, the JWT key and the encryption key belong in machine-level environment variables or in appsettings.Production.json with restricted permissions, not in a file that ships with the site.

Proxied instead

If the backend runs on its own, replace the handler block with a rewrite rule and enable WebSocket on the site:

<rewrite>
  <rules>
    <rule name="Proxy to the backend" stopProcessing="true">
      <match url="(.*)" />
      <action type="Rewrite" url="http://127.0.0.1:10026/{R:1}" />
      <serverVariables>
        <set name="HTTP_X_FORWARDED_PROTO" value="https" />
      </serverVariables>
    </rule>
  </rules>
</rewrite>

HTTP_X_FORWARDED_PROTO must be added to the allowed server variables of the site before a rule can set it, otherwise IIS returns 500.

Verify

curl.exe -sf https://api.example.com/health/ready
curl.exe -sI https://app.example.com/ | findstr /i cache-control

Then open the app host and sign in. A page that loads while every call fails points at ApiBaseUrl or Cors__Origins__0, not at IIS.