Learnixo
Back to blog
Backend Systemsintermediate

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.

LearnixoJune 4, 20267 min read
.NETC#IISDeploymentWindows ServerASP.NET CoreDevOps
Share:š•

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)
POWERSHELL
# 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 w3svc

Verify ANCM is installed: Open IIS Manager → Server node → Modules → Look for AspNetCoreModuleV2

2. Enable Required IIS Features

POWERSHELL
# 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-WebSockets

Publish the Application

Bash
# 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 app

IIS 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: Integrated

Via PowerShell:

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.com

Via PowerShell:

POWERSHELL
New-Website -Name "OrderFlow API" `
    -ApplicationPool "OrderFlowAppPool" `
    -PhysicalPath "C:\inetpub\orderflow\publish" `
    -Port 443 `
    -HostHeader "api.orderflow.com" `
    -Ssl

3. Set Folder Permissions

The application pool identity needs read access to the publish folder:

POWERSHELL
$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
<?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.

XML
<!-- 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:

POWERSHELL
[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

POWERSHELL
# Download win-acme from https://www.win-acme.com/
# Run and follow prompts
.\wacs.exe

Option 3: Import Certificate via PowerShell

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

XML
<!-- 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):

XML
<aspNetCore stdoutLogEnabled="true"
            stdoutLogFile="C:\inetpub\orderflow\logs\stdout">
</aspNetCore>

Configure Serilog to write to files:

C#
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

POWERSHELL
# 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:

  1. Enable stdout logging in web.config
  2. Check C:\inetpub\orderflow\logs\stdout_*.log
  3. Common causes: missing .NET Hosting Bundle, wrong processPath, missing DLL

HTTP 403.14 — directory listing:

  • web.config not found — ensure publish output includes it

App starts but returns 502:

  • App pool crashed — check Windows Event Viewer → Application log

App not serving requests:

POWERSHELL
# 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 enabled

Interview 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?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.