Deploy ASP.NET Core Web API on IIS: Step-by-Step Guide
Deploy ASP.NET Core Web API to IIS on Windows Server. Covers .NET Hosting Bundle, application pool configuration, web.config, HTTPS, environment variables, logging, troubleshooting, and production checklist.
IIS vs Other Hosting Options
| Option | OS | Complexity | Use when | |---|---|---|---| | IIS | Windows only | Medium | Enterprise Windows Server, existing IIS setup | | Docker | Any | Low-Medium | Cloud, containerised environments | | Azure App Service | Cloud | Low | Azure-first, managed hosting | | Kestrel | Any | Low | Linux, containers | | Nginx | Linux | Medium | Linux reverse proxy |
IIS is the right choice when you're on Windows Server infrastructure, your organisation manages its own servers, or you need integration with Windows features (Windows Auth, HTTPS certificates via IIS).
Prerequisites
1. Install .NET Hosting Bundle on Windows Server
The .NET Hosting Bundle includes:
- .NET Runtime
- ASP.NET Core Runtime
- IIS ASP.NET Core Module (ANCM)
# Download and install .NET 9 Hosting Bundle
# From: https://dotnet.microsoft.com/download/dotnet/9.0
# File: dotnet-hosting-{version}-win.exe
# After install, restart IIS
net stop was /y
net start w3svcVerify ANCM is installed:
Open IIS Manager ā Server node ā Modules ā Look for AspNetCoreModuleV2
2. Enable Required IIS Features
# Enable IIS features via PowerShell
Enable-WindowsOptionalFeature -Online -FeatureName `
IIS-WebServerRole, `
IIS-WebServer, `
IIS-CommonHttpFeatures, `
IIS-HttpErrors, `
IIS-ApplicationDevelopment, `
IIS-HealthAndDiagnostics, `
IIS-HttpLogging, `
IIS-RequestMonitor, `
IIS-Security, `
IIS-RequestFiltering, `
IIS-Performance, `
IIS-StaticContent, `
IIS-DefaultDocument, `
IIS-DirectoryBrowsing, `
IIS-WebSocketsPublish the Application
# Publish as framework-dependent (requires .NET on server)
dotnet publish -c Release -o ./publish
# Or self-contained (includes .NET runtime ā no install needed on server)
dotnet publish -c Release -r win-x64 --self-contained true -o ./publish
# The /publish folder contains everything needed to run the appIIS Site Configuration
1. Create Application Pool
IIS Manager ā Application Pools ā Add Application Pool
Name: OrderFlowAppPool
.NET CLR version: No Managed Code ā important for .NET Core/5+
Managed pipeline mode: IntegratedVia PowerShell:
Import-Module WebAdministration
New-WebAppPool -Name "OrderFlowAppPool"
Set-ItemProperty IIS:\AppPools\OrderFlowAppPool -Name managedRuntimeVersion -Value ""
Set-ItemProperty IIS:\AppPools\OrderFlowAppPool -Name processModel.identityType -Value "ApplicationPoolIdentity"2. Create the Website
IIS Manager ā Sites ā Add Website
Site name: OrderFlow API
Application pool: OrderFlowAppPool
Physical path: C:\inetpub\orderflow\publish
Port: 80 (HTTP) or 443 (HTTPS)
Host name: api.orderflow.comVia PowerShell:
New-Website -Name "OrderFlow API" `
-ApplicationPool "OrderFlowAppPool" `
-PhysicalPath "C:\inetpub\orderflow\publish" `
-Port 443 `
-HostHeader "api.orderflow.com" `
-Ssl3. Set Folder Permissions
The application pool identity needs read access to the publish folder:
$acl = Get-Acl "C:\inetpub\orderflow\publish"
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
"IIS AppPool\OrderFlowAppPool", "ReadAndExecute", "ContainerInherit,ObjectInherit", "None", "Allow")
$acl.SetAccessRule($rule)
Set-Acl "C:\inetpub\orderflow\publish" $acl
# Also grant write access to the logs folder
icacls "C:\inetpub\orderflow\logs" /grant "IIS AppPool\OrderFlowAppPool:(OI)(CI)W"web.config
ASP.NET Core uses web.config as the IIS bridge ā it tells IIS to hand requests to Kestrel via the ANCM:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*"
modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet"
arguments=".\OrderFlow.API.dll"
stdoutLogEnabled="true"
stdoutLogFile=".\logs\stdout"
hostingModel="inprocess">
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
<environmentVariable name="ConnectionStrings__Default"
value="Server=.;Database=OrderFlow;Trusted_Connection=True;" />
</environmentVariables>
</aspNetCore>
</system.webServer>
</location>
</configuration>Key settings:
hostingModel="inprocess"ā runs in the IIS worker process (faster, recommended)hostingModel="outofprocess"ā runs in a separate Kestrel process (more isolated)stdoutLogEnabled="true"ā enables startup logging (set to false in production after verifying)
Environment Variables and Secrets
Never put passwords in web.config ā it may be served as a file.
<!-- Use IIS environment variables -->
<environmentVariables>
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Production" />
</environmentVariables>Set sensitive variables in IIS Manager:
IIS Manager ā Site ā Configuration Editor
ā system.webServer/aspNetCore/environmentVariables
ā Add: ConnectionStrings__Default = "Server=..."Or use Windows Environment Variables:
[System.Environment]::SetEnvironmentVariable(
"ConnectionStrings__Default",
"Server=prod-sql;Database=OrderFlow;User Id=app;Password=...;",
"Machine")HTTPS Configuration
Option 1: IIS Binding with Certificate
IIS Manager ā Site ā Bindings ā Add
Type: https
IP address: All Unassigned
Port: 443
Host name: api.orderflow.com
SSL certificate: [select your certificate]Option 2: Let's Encrypt via Win-ACME
# Download win-acme from https://www.win-acme.com/
# Run and follow prompts
.\wacs.exeOption 3: Import Certificate via PowerShell
# Import PFX certificate
$cert = Import-PfxCertificate `
-FilePath "certificate.pfx" `
-CertStoreLocation "cert:\LocalMachine\My" `
-Password (ConvertTo-SecureString "pfxPassword" -AsPlainText -Force)
# Bind to IIS site
New-WebBinding -Name "OrderFlow API" -Protocol "https" -Port 443 -HostHeader "api.orderflow.com"
$binding = Get-WebBinding -Name "OrderFlow API" -Protocol "https"
$binding.AddSslCertificate($cert.Thumbprint, "my")Redirect HTTP to HTTPS
<!-- web.config ā add URL rewrite rule -->
<system.webServer>
<rewrite>
<rules>
<rule name="HTTP to HTTPS redirect" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}"
redirectType="Permanent" />
</rule>
</rules>
</rewrite>
</system.webServer>Logging
Enable application logging in web.config (use only for troubleshooting):
<aspNetCore stdoutLogEnabled="true"
stdoutLogFile="C:\inetpub\orderflow\logs\stdout">
</aspNetCore>Configure Serilog to write to files:
Log.Logger = new LoggerConfiguration()
.WriteTo.File(
@"C:\inetpub\orderflow\logs\app-.log",
rollingInterval: RollingInterval.Day)
.CreateLogger();Grant write permissions to the logs folder for the app pool identity (see Folder Permissions above).
Deploying Updates
# 1. Stop the site (optional ā inprocess model may need this)
Stop-Website -Name "OrderFlow API"
# 2. Copy new publish files
robocopy ".\publish" "C:\inetpub\orderflow\publish" /MIR /XD logs
# 3. Restart the app pool
Restart-WebAppPool -Name "OrderFlowAppPool"
# 4. Start the site
Start-Website -Name "OrderFlow API"Troubleshooting
HTTP 500.30 ā failed to start:
- Enable stdout logging in
web.config - Check
C:\inetpub\orderflow\logs\stdout_*.log - Common causes: missing .NET Hosting Bundle, wrong
processPath, missing DLL
HTTP 403.14 ā directory listing:
web.confignot found ā ensure publish output includes it
App starts but returns 502:
- App pool crashed ā check Windows Event Viewer ā Application log
App not serving requests:
# Check app pool state
Get-WebConfiguration "system.applicationHost/applicationPools/add[@name='OrderFlowAppPool']/@state"
# Recycle app pool
Restart-WebAppPool -Name "OrderFlowAppPool"Production Checklist
ā .NET Hosting Bundle installed on server
ā Application pool set to "No Managed Code"
ā HTTPS binding configured with valid certificate
ā HTTP ā HTTPS redirect in place
ā Folder permissions set for IIS AppPool identity
ā Sensitive config in IIS environment variables or Windows env (not web.config)
ā ASPNETCORE_ENVIRONMENT = "Production"
ā stdoutLogEnabled = "false" (enable only for troubleshooting)
ā Logs folder with write permissions
ā Firewall allows 443, blocks direct 5000/5001 access
ā Windows Firewall configured
ā App pool auto-restart on crash enabledInterview Questions
Q: What is the ASP.NET Core Module (ANCM) and why is it needed for IIS? IIS natively only processes requests via the .NET Framework pipeline. ANCM bridges IIS and ASP.NET Core's Kestrel web server. In in-process mode, Kestrel runs inside the IIS worker process. In out-of-process mode, IIS forwards requests to a separate Kestrel process via a reverse proxy.
Q: What is the difference between in-process and out-of-process hosting on IIS? In-process runs the app in the IIS worker process (w3wp.exe) ā lower latency, simpler setup. Out-of-process runs Kestrel separately ā IIS acts as a reverse proxy. In-process is recommended for most scenarios; out-of-process gives more isolation.
Q: Why set the Application Pool to "No Managed Code"? ASP.NET Core doesn't use the .NET Framework CLR that IIS traditionally loaded. Setting it to "No Managed Code" tells IIS not to load any CLR version ā the ANCM handles loading the .NET Core runtime instead. Loading the wrong CLR can cause startup failures.
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.