If you still ship a different build per environment just to flip ASPNETCORE_ENVIRONMENT, you are paying a tax you do not need. On Windows/IIS, the host process can inject environment variables before your ASP.NET Core app starts. One publish output, correct config per site.

That matters on shared and VPS Windows hosts where you may promote the same artifact from a smoke site to production, or run side-by-side slots under different hostnames. Get the environment wrong and you load the wrong connection strings, log levels, and feature flags—with no compile error to save you.

#What IIS actually starts

With the ASP.NET Core Module (ANCM), IIS is the entry point whether you use in-process (HostingModel=inprocess) or out-of-process Kestrel. ANCM reads the aspNetCore section in web.config, sets process environment variables, then starts the worker. launchSettings.json is a Visual Studio/dev convenience; it is not applied on the server.

For .NET 10 apps the pattern is unchanged: Prefer environment variables and appsettings.{Environment}.json over compile-time constants. Keep secrets out of the package—connection strings and API keys belong in IIS configuration, user secrets locally, or a vault your ops process injects at deploy time.

#Set it in web.config (site-scoped)

The most portable approach on shared Windows hosting is the environmentVariables child of aspNetCore. It travels with the site and does not require machine admin rights:

xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <location path="." inheritInChildApplications="false">
    <system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" />
      </handlers>
      <aspNetCore processPath="dotnet"
                  arguments=".\MyApp.dll"
                  stdoutLogEnabled="false"
                  stdoutLogFile=".\logs\stdout"
                  hostingModel="inprocess">
        <environmentVariables>
          <environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Staging" />
          <environmentVariable name="ASPNETCORE_DETAILEDERRORS" value="false" />
        </environmentVariables>
      </aspNetCore>
    </system.webServer>
  </location>
</configuration>

After an edit, recycle the app pool (or touch web.config again) so ANCM restarts the worker. Confirm with a temporary diagnostic endpoint or startup log line that reads Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")—then remove the diagnostic before you leave Staging.

#App pool variables when you manage the server

On Windows Server 2025 boxes you control (VPS/dedicated), app-pool-level environment variables keep web.config thinner and survive publish overwrites. Set them once per pool:

powershell
# Run elevated on the IIS box
Import-Module WebAdministration

$pool = "MyAppPool"
# IIS 10 configuration path for app pool environment variables
Set-WebConfigurationProperty `
  -PSPath "MACHINE/WEBROOT/APPHOST" `
  -Filter "system.applicationHost/applicationPools/add[@name='$pool']/environmentVariables" `
  -Name "." `
  -Value @{ name = "ASPNETCORE_ENVIRONMENT"; value = "Production" }

Restart-WebAppPool -Name $pool

If the collection already exists, add or update the named entry instead of replacing the whole collection. Prefer one pool per app so Staging and Production never share worker process state or environment.

#Publish and transform gotchas

  • Web Deploy and folder publish can overwrite web.config. Keep environmentVariable entries in a transform you control, or re-apply them in the release pipeline after sync.
  • Do not rely on ASPNETCORE_ENVIRONMENT inside the published appsettings alone—without the process env var, the host defaults to Production and will ignore appsettings.Staging.json.
  • In-process hosting shares the w3wp environment; out-of-process still gets ANCM-injected variables on the child dotnet process. Either model works if ANCM is configured.
  • Map IHostEnvironment in code (env.IsProduction(), env.IsEnvironment("Staging")) rather than string-comparing machine names. That keeps the same binaries portable across IIS sites.

A minimal Program.cs check that fails fast in the wrong place saves overnight fire drills:

csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    // Never enable dev exception pages on an IIS internet site
    throw new InvalidOperationException(
        "Development environment is not allowed on this host.");
}

app.MapGet("/_env", (IHostEnvironment env) => Results.Ok(new
{
    env.EnvironmentName,
    env.ApplicationName
}));

app.Run();

Lock /_env down (IP allow list, auth, or remove after verification). On multi-tenant Windows hosts, exposing environment names publicly is unnecessary reconnaissance.

#Configuration layering that behaves on IIS

Default host builder order still applies: appsettings.json, appsettings.{Environment}.json, user secrets (Development only), environment variables, command line. IIS-injected values win over files for the same key when you use the standard __ (double underscore) hierarchy for nested settings, e.g. ConnectionStrings__AppDb. That is often cleaner than rewriting JSON on the server.

For SQL Server connection strings on Windows hosting, keep the string out of source control and inject it the same way—pool or site environment variable—or via the IIS connection string collection your deploy docs already use. Rotate passwords by changing the server-side value and recycling the pool, not by republishing the app.

Practical takeaway: publish once for .NET 10, set ASPNETCORE_ENVIRONMENT (and secrets) at the IIS site or app pool, recycle, and verify with a gated diagnostic. Treat web.config environmentVariables as part of your release checklist so the next Web Deploy does not silently drop you back to the wrong environment.