Articles › Git & DevOps
Deploy ASP.NET Core to IIS on Windows Server: Step-by-Step Guide
Host ASP.NET Core and React on IIS: Hosting Bundle, publishing, application pools, HTTPS, SPA routing, zero-downtime deploys and fixing 500.30 errors.
Many companies — especially in Iran — host their applications on Windows servers with IIS. ASP.NET Core runs very well on IIS, but the setup is different from the old ASP.NET Framework days: IIS acts as a front-end server that hands requests to your .NET app through the ASP.NET Core Module (ANCM). This guide walks through a complete, production-ready deployment.
How ASP.NET Core runs on IIS
There are two hosting models:
| Model | How it works | Notes |
|---|---|---|
| In-process (default) | your app runs inside the IIS worker process (w3wp.exe) |
fastest; one app per application pool |
| Out-of-process | IIS proxies requests to your app running on Kestrel in a separate process | useful for isolation or special cases |
Either way, the ASP.NET Core Module — installed by the Hosting Bundle — is what connects IIS to .NET.
Step 1: prepare the server
- Enable IIS: Server Manager → Add Roles and Features → Web Server (IIS). Include WebSocket Protocol if you use SignalR.
- Install the .NET Hosting Bundle for your exact major version (e.g. .NET 8) from the official .NET download page. It installs the runtime and the ASP.NET Core Module.
- Restart IIS so it picks up the module:
net stop was /y
net start w3svc
Warning: Install the Hosting Bundle after enabling IIS. If you installed it before, run the installer again and choose Repair — otherwise the module isn't registered and you'll get HTTP 500.19 errors.
Step 2: publish the application
dotnet publish -c Release -o ./publish
Options worth knowing:
- Framework-dependent (default): small output, uses the runtime installed on the server.
- Self-contained (
--self-contained -r win-x64): includes the runtime; no runtime install needed, but a bigger output.
If the React frontend is served by the same app, build it and copy the output into wwwroot before publishing:
cd frontend && npm ci && npm run build
cp -r dist/* ../backend/wwwroot/
cd ../backend && dotnet publish -c Release -o ./publish
The publish output includes a generated web.config:
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\MyApp.dll"
stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout"
hostingModel="inprocess" />
</system.webServer>
</location>
</configuration>
Step 3: create the site and application pool
- Copy the publish folder to the server, e.g.
C:\inetpub\sites\myapp. - In IIS Manager → Application Pools → Add: name
MyAppPool, .NET CLR version: No Managed Code (ASP.NET Core doesn't use the CLR integration of IIS), pipeline Integrated. - Sites → Add Website: physical path
C:\inetpub\sites\myapp, application poolMyAppPool, binding with your host name (e.g.example.com, port 80).
Or script it:
Import-Module WebAdministration
New-WebAppPool -Name "MyAppPool"
Set-ItemProperty IIS:\AppPools\MyAppPool -Name managedRuntimeVersion -Value ""
Set-ItemProperty IIS:\AppPools\MyAppPool -Name startMode -Value "AlwaysRunning"
New-Website -Name "MyApp" -PhysicalPath "C:\inetpub\sites\myapp" -ApplicationPool "MyAppPool" -HostHeader "example.com" -Port 80
Folder permissions
The app pool runs as the identity IIS AppPool\MyAppPool. Grant it read on the site folder and modify only on folders the app writes to — uploads, logs, a SQLite database:
icacls "C:\inetpub\sites\myapp" /grant "IIS AppPool\MyAppPool:(OI)(CI)R"
icacls "C:\inetpub\sites\myapp\wwwroot\uploads" /grant "IIS AppPool\MyAppPool:(OI)(CI)M"
Tip: Keep databases and user uploads outside the deployment folder (for example
C:\ProgramData\MyApp) and point to them through configuration. Then redeploying never overwrites data.
Step 4: configuration and secrets
The environment defaults to Production. Set environment variables per site in web.config (or in IIS Configuration Editor):
<aspNetCore processPath="dotnet" arguments=".\MyApp.dll" hostingModel="inprocess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
<environmentVariable name="ConnectionStrings__Default" value="Server=.;Database=MyApp;Trusted_Connection=True;TrustServerCertificate=True" />
</environmentVariables>
</aspNetCore>
Use appsettings.Production.json for non-secret settings. For secrets, prefer machine-level environment variables or a secret store over files in the site folder. If you connect to SQL Server with Windows authentication, grant the app pool identity a login in SQL Server.
Step 5: HTTPS
- Get a certificate — from your provider, or free with Let's Encrypt using a Windows ACME client such as win-acme, which also renews automatically.
- Add an https binding on port 443 with the certificate and enable Server Name Indication if several sites share the IP.
- Redirect HTTP to HTTPS and enable HSTS in the app:
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
app.UseHttpsRedirection();
}
Step 6: SPA routing and static files
When React Router handles routes like /en/articles/my-post, a page refresh asks the server for that path. The app must return index.html for unknown non-API routes:
app.UseDefaultFiles();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// hashed assets can be cached forever; index.html must not be
if (ctx.File.Name != "index.html")
ctx.Context.Response.Headers.CacheControl = "public,max-age=31536000,immutable";
}
});
app.MapControllers();
app.MapFallbackToFile("index.html");
Warning: The Vite build may include its own
web.configwith URL Rewrite rules meant for static hosting. When the frontend is served by ASP.NET Core, remove it fromwwwroot— a second web.config inside the site can conflict with the ASP.NET Core handler.
Enable response compression (dynamic compression in IIS, or app.UseResponseCompression()), and make sure IIS serves modern MIME types such as .webp, .avif and .woff2 (ASP.NET Core's static file middleware already knows them).
Step 7: keep it warm and stable
By default IIS stops idle app pools after 20 minutes, so the next visitor waits for a cold start. For production:
- Application pool → Start Mode: AlwaysRunning, Idle Time-out: 0.
- Site → Preload Enabled: True (requires the Application Initialization feature).
- Disable the default periodic recycling (every 1740 minutes) or schedule it at a quiet hour.
- For SignalR, enable the WebSocket Protocol feature.
Zero-downtime-ish deployments
Replacing DLLs while the app runs fails because files are locked. Place an app_offline.htm in the site folder — ANCM gracefully shuts the app down and serves that page — copy the new files, then delete it:
Copy-Item .\maintenance.htm C:\inetpub\sites\myapp\app_offline.htm
robocopy .\publish C:\inetpub\sites\myapp /MIR /XD uploads logs /XF app_offline.htm
Remove-Item C:\inetpub\sites\myapp\app_offline.htm
/MIR mirrors the folder; /XD excludes folders with runtime data. For true zero downtime, run two sites behind ARR or a load balancer and switch between them.
Troubleshooting
| Error | Usual cause | Fix |
|---|---|---|
| 500.19 | ANCM not installed / invalid web.config | install or repair the Hosting Bundle |
| 500.30 — in-process start failure | app crashed during startup (bad config, missing DB, exception in Program.cs) |
check logs (below) |
| 500.31 | the required .NET runtime isn't installed | install the right Hosting Bundle version |
| 500.32 / 500.35 | bitness mismatch / two in-process apps in one pool | match 32/64-bit; one app per pool |
| 502.5 | out-of-process app failed to start | same as 500.30 |
| 403.14 | directory browsing — app isn't handling the request | handler mapping missing; check web.config |
To see startup errors:
- Windows Event Viewer → Windows Logs → Application, source IIS AspNetCore Module V2.
- Temporarily set
stdoutLogEnabled="true"in web.config (and create thelogsfolder with write permission), reproduce, read the log, then turn it off again. - Run the app directly on the server:
dotnet .\MyApp.dll— exceptions print to the console. - Add structured logging (Serilog to a file or Seq) so production issues are visible after startup too.
Summary
To host ASP.NET Core on IIS: enable IIS, install the matching Hosting Bundle, publish (with the React build in wwwroot if you serve the SPA from the same app), create a No Managed Code application pool and grant it minimal folder permissions, configure the environment and secrets, add HTTPS, use a SPA fallback, keep the pool always running, and deploy with app_offline.htm. When something fails, Event Viewer and stdout logs point to the cause.
FAQ
IIS or Linux with Nginx?
Both are production-ready. IIS is natural if your infrastructure, SQL Server and team are Windows-based; Linux with Nginx (or containers) is common for cloud and cost-sensitive hosting. The app code is the same.
Can I host multiple ASP.NET Core apps on one IIS server?
Yes. Give each app its own application pool (required for in-process hosting) and its own site or virtual application.
Do I need to install the .NET SDK on the server?
No. The server needs only the Hosting Bundle (runtime + module). Build and publish on your development machine or CI.