Plenty of .NET teams ship to Windows Server, and there IIS remains the front door. Hosting ASP.NET Core in IIS is different from the old Framework days — your app runs as its own process behind the ASP.NET Core Module, not inside w3wp — and understanding that model turns the classic 500.19/500.30 troubleshooting sessions into five-minute fixes. This guide covers publish, site setup, the app pool settings that matter, and the errors everyone meets.

How hosting actually works
ASP.NET Core apps are self-contained Kestrel web servers. IIS sits in front via the ASP.NET Core Module (ANCM) in one of two modes:
- In-process (default): your app loads inside the IIS worker process. Fastest — no proxy hop.
- Out-of-process: IIS reverse-proxies to Kestrel running as a separate process. Slightly slower; needed only for edge cases (e.g. multiple apps sharing one process model constraint).
Stay with in-process unless you have a reason. Either way, web.config is generated at publish time to tell ANCM how to launch you — you rarely edit it by hand.
Prerequisites on the server
- Install IIS (Server Manager → Web Server role, or
Enable-WindowsOptionalFeature -Online -FeatureName IIS-WebServer). - Install the .NET Hosting Bundle for your runtime version (search "dotnet hosting bundle" on Microsoft's download page) — it contains ANCM plus the runtime. Install it after IIS, or repair it, or ANCM won't register.
iisresetafter installing.
The Hosting Bundle version must be ≥ your app's target framework. A 500.31 ("Failed to load ASP.NET Core runtime") almost always means the bundle is older than the app.
Publish the app
From your project (or CI, per the CI/CD article):
dotnet publish -c Release -o ./publish
Framework-dependent publish (the default) keeps output small and patches the runtime server-side via the Hosting Bundle; --self-contained true -r win-x64 bundles the runtime per-app instead — bigger, but immune to server runtime versions. Visual Studio's right-click Publish with a Folder or Web Deploy profile produces the same output; Web Deploy (msdeploy) can push straight to IIS including app-offline handling.
The publish folder contains your DLLs plus the generated web.config:
<aspNetCore processPath="dotnet" arguments=".\MyApi.dll" stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
Create the site
In IIS Manager:
- Application Pools → Add — name it after the app, and set .NET CLR Version = "No Managed Code". ASP.NET Core doesn't use the CLR loaded by IIS; leaving v4.0 selected is harmless noise, but No Managed Code states the truth.
- Sites → Add Website — point the physical path at the publish folder, bind host name/port, pick the new pool.
- Grant the pool identity read access to the folder: the identity is
IIS AppPool\<PoolName>(add it in the folder's Security tab).
App pool settings worth changing from defaults: Start Mode = AlwaysRunning and Idle Time-out = 0 for APIs (the default 20-minute idle recycle gives your first user after lunch a cold start), and consider disabling the periodic 29-hour recycle in favor of scheduled recycling at low-traffic hours.
The troubleshooting table
| Error | Meaning | Fix |
|---|---|---|
| 500.19 | web.config can't be read | Hosting Bundle missing (ANCM not registered) or folder ACLs |
| 500.30 | app failed to start | Run dotnet MyApi.dll in the folder — the console shows the real exception |
| 500.31 | can't load runtime | Hosting Bundle older than app's target framework |
| 502.5 (out-of-process) | Kestrel process died | Same as 500.30 — start it manually and read the error |
| 403.14 | IIS wants a directory listing | Site's physical path points at the wrong folder (not the publish output) |
The universal move is the second row: run the app directly on the server (dotnet MyApi.dll from the publish folder). IIS hides startup exceptions; the console prints them. For persistent capture, flip stdoutLogEnabled="true" temporarily — and turn it back off, because stdout logs grow without rotation. Real logging belongs to Serilog with rolling files.
Environment, HTTPS, and deployment hygiene
- Set
ASPNETCORE_ENVIRONMENT=Production(and secrets like connection strings) via Configuration Editor → system.webServer/aspNetCore → environmentVariables, or machine-level environment variables — not by editing appsettings.json on the server. - Bind the HTTPS certificate in IIS (Bindings → https → select cert); IIS terminates TLS and ANCM forwards the scheme so
Request.IsHttpsbehaves. - Deploying over a running site fails on locked DLLs: drop an
app_offline.htminto the folder (IIS gracefully stops the app, serves that page, and releases locks), copy files, delete it. Web Deploy and most CI tasks automate exactly this.
When IIS is still the right answer
On Windows infrastructure you already operate, IIS gives you process supervision, TLS management, and ops familiarity for free. Outside it, the same publish output runs behind Nginx/Caddy on Linux, or in a container — geeksarray.com itself ships that way. The app doesn't change; only the front door does — which is precisely what the Kestrel-behind-a-proxy model was designed for.
Comments (0)
No comments yet — be the first to share your thoughts.