Web & Application Hosting
Web & Application Hostingintermediate18 min read·Updated Apr 30, 2026

How to Host ASP.NET Core 10 on IIS — Windows Server Guide (2026)

Deploy an ASP.NET Core 10 app on IIS using a Windows Server VPS. This guide covers IIS, the .NET 10 Hosting Bundle, app pool setup, deployment, firewall rules, and common errors — tested end-to-end on Raff.

Genuine, fully-licensed Windows
Spin up a Windows Server: full admin, RDP-ready
Genuine Windows Server 2019, 2022, or 2025 with full administrator access. We handle the Microsoft licensing, billed monthly with nothing to buy upfront.
IIS welcome page on Windows Server 2025

In short

To host ASP.NET Core 10 on IIS, you need a Windows Server environment, the IIS role, the .NET 10 Hosting Bundle, a published ASP.NET Core app, and an IIS app pool configured with No Managed Code.

This guide shows the full deployment flow on a Raff Windows VM:

Text
Browser → Windows Server → IIS → ASP.NET Core Module V2 → ASP.NET Core 10 app

If you are here for the .NET 10 Hosting Bundle, install it on the same Windows Server that will run your IIS site. The Hosting Bundle gives IIS the runtime components and ASP.NET Core Module V2 required to launch your app. ASP.NET Core 10 app hosted on IIS using ASP.NET Core Module V2 on a Raff Windows VM

Need a Windows VM for ASP.NET Core and IIS?
Deploy a Raff Windows VM with NVMe storage, unmetered bandwidth, RDP access, and browser console support.
View Windows VM plans

Who this guide is for

You want to host an ASP.NET Core web app on Windows Server with IIS as the front-end web server. Common scenarios:

  • A small business app, intranet, customer portal, or dashboard that needs to run on a single Windows Server
  • An ASP.NET Core API behind IIS, where IIS handles HTTPS, static assets, and request routing while your app handles business logic
  • Multiple ASP.NET Core sites on one server, each with their own IIS bindings and app pools
  • Replacing an aging ASP.NET Framework deployment with a modern .NET 10 codebase
  • Testing a Windows VM before moving an ASP.NET Core app into production

If you're hosting at scale with multiple servers, load balancers, or container orchestration, Kestrel behind a reverse proxy or a container-based architecture may be a better fit than a single IIS server. This guide is for the common single-server or small-cluster case where IIS gives you HTTPS termination, static asset serving, bindings, and process management in a familiar Windows Server environment.

What you'll need

  • A Windows Server 2025 or Windows Server 2022 machine. This guide was tested on a Raff Windows VM.
  • Local administrator access through RDP. See our RDP connection guide if you haven't connected before.
  • An ASP.NET Core app to deploy. We'll create one as part of this walkthrough using dotnet new webapp.
  • The .NET 10 Hosting Bundle installed on the server.
  • Optional but recommended: a separate machine for building such as your laptop, a CI runner, or a build server. Production servers should usually host apps, not compile them.
  • Estimated time: 30-45 minutes for a first deployment, and much faster for future deployments after the server is prepared.

For a small staging app, internal tool, dashboard, or low-traffic website, a smaller Windows VM can be enough. For production apps, multiple IIS sites, SQL Server on the same machine, or heavier workloads, choose more CPU, RAM, and NVMe storage.

What changed in .NET 10 / 2026

If you're coming from older guides written for .NET 6, 7, 8, or 9, three things are different:

  1. .NET 10 is the current LTS target for new deployments. LTS releases receive support for three years. For new production ASP.NET Core deployments, target .NET 10 unless you have a specific compatibility reason not to.
  2. The Hosting Bundle and SDK are different packages. The Hosting Bundle lets IIS run ASP.NET Core apps. The SDK is required for dotnet new, dotnet build, and dotnet publish.
  3. The app pool should be set to No Managed Code. ASP.NET Core does not run through the classic .NET Framework CLR app pool runtime. IIS uses ASP.NET Core Module V2 to start and host the app.

Step 1 — Install the Web Server (IIS) role

Open PowerShell as administrator on the Raff Server and run:

Powershell
Install-WindowsFeature -Name ` Web-Server, ` Web-Common-Http, ` Web-Static-Content, ` Web-Default-Doc, ` Web-Dir-Browsing, ` Web-Http-Errors, ` Web-App-Dev, ` Web-Net-Ext45, ` Web-AppInit, ` Web-ISAPI-Ext, ` Web-ISAPI-Filter, ` Web-Health, ` Web-Http-Logging, ` Web-Performance, ` Web-Stat-Compression, ` Web-Security, ` Web-Filtering, ` Web-Mgmt-Tools, ` Web-Mgmt-Console ` -IncludeManagementTools

This installs the IIS parent role plus the modules ASP.NET Core needs, including Common HTTP, App Development, ISAPI filters/extensions for ANCM V2, request filtering, dynamic compression, logging, and the management console.

Verify:

Powershell
Get-WindowsFeature -Name Web-Server | Format-Table Name, InstallState Get-Service -Name W3SVC, WAS | Format-Table Name, Status, StartType

You should see Web-Server: Installed, W3SVC: Running, and WAS: Running.

The Manual start type for WAS (Windows Process Activation Service) is normal. It is started on demand by IIS. Do not change it just because it says Manual.

Confirm IIS is serving requests by browsing http://localhost from Edge on the server:

IIS welcome page on Windows Server 2025 — first run after Install-WindowsFeature

If you see the multi-language "Internet Information Services" welcome page, IIS is alive.

Article finding from our test: Windows Server 2025 IIS install completed without requiring a reboot. Older Windows Server versions may sometimes prompt for one depending on installed roles and pending updates.

Step 2 — Install the .NET 10 Hosting Bundle

The Hosting Bundle is a single installer that puts down the pieces IIS needs to run ASP.NET Core apps:

  • .NET Runtime — the runtime that runs managed .NET code
  • ASP.NET Core Runtime — the ASP.NET Core framework runtime
  • ASP.NET Core Module V2 — the IIS module that starts and routes requests to your ASP.NET Core app

The fastest way is winget:

Powershell
winget install --id Microsoft.DotNet.HostingBundle.10 --silent --accept-source-agreements --accept-package-agreements

The exact patch version may be newer than the examples in this guide. That is normal. Install the latest available .NET 10 Hosting Bundle unless your app requires a specific patched runtime version.

If winget isn't available or you prefer a GUI install, download the bundle from Microsoft's official .NET 10 download page:

Text
https://dotnet.microsoft.com/download/dotnet/10.0

Choose:

Text
ASP.NET Core Runtime → Hosting Bundle

The filename is usually similar to:

Text
dotnet-hosting-10.0.x-win.exe

Run the installer as administrator.

Important: install IIS before the Hosting Bundle

For the cleanest setup, install IIS first, then install the .NET 10 Hosting Bundle.

If the Hosting Bundle is installed before IIS, repair or run the Hosting Bundle installer again after IIS is installed so ASP.NET Core Module V2 is registered correctly with IIS.

This guide installs IIS first, then the Hosting Bundle, to avoid that problem.

Refresh PATH in your current PowerShell session

The Hosting Bundle adds C:\Program Files\dotnet\ to the Machine PATH, but your existing PowerShell session may not see it until you reopen the window.

You can refresh PATH manually:

Powershell
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")

This is a common gotcha. If your dotnet command says "not recognized" right after installing the bundle, this is usually why.

Verify the runtime is registered

Powershell
dotnet --list-runtimes

You should see entries similar to:

Text
Microsoft.AspNetCore.App 10.0.x [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 10.0.x [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]

Verify ANCM is registered with IIS

Powershell
Import-Module WebAdministration Get-WebGlobalModule -Name "AspNetCoreModuleV2" | Format-Table Name, Image

Expected output:

Text
Name Image ---- ----- AspNetCoreModuleV2 %ProgramFiles%\IIS\Asp.Net Core Module\V2\aspnetcorev2.dll

If this returns nothing, ASP.NET Core Module V2 is not registered correctly. Re-run or repair the Hosting Bundle installer as administrator.

The Hosting Bundle does not replace a Windows server

The .NET 10 Hosting Bundle gives IIS the runtime components required to run ASP.NET Core apps. It does not provide the server itself.

For a real deployment, you still need a Windows environment with:

  • IIS installed
  • RDP access
  • enough CPU and RAM for the app
  • firewall control
  • storage for app files, logs, and uploads
  • backups or snapshots
  • a domain and HTTPS certificate for production

That is why many small teams, agencies, MSPs, and internal software teams deploy ASP.NET Core apps on a Windows VM: they get direct control over IIS, app pools, bindings, certificates, logs, and deployment folders without managing physical hardware.

Step 3 — Restart IIS

After the Hosting Bundle is installed, restart IIS:

Powershell
iisreset

Expected output:

Text
Internet services successfully stopped Internet services successfully restarted

On a fresh server, your app may work without this restart. But running iisreset removes uncertainty and ensures IIS loads the ASP.NET Core Module correctly.

If you have production apps already running on the server, schedule this carefully because iisreset briefly interrupts IIS-hosted sites.

Step 4 — Publish an ASP.NET Core app

Two paths are available. Pick whichever fits your workflow.

Path A — Build on the same server (dev/staging only)

This is convenient for a single-server test, but it is not the cleanest production pattern. Production servers should usually run apps, not compile them.

You'll need the .NET 10 SDK, which is separate from the Hosting Bundle:

Powershell
winget install --id Microsoft.DotNet.SDK.10 --silent --accept-source-agreements --accept-package-agreements # Refresh PATH for the current session $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") # Verify SDK is available dotnet --list-sdks

You should see a .NET 10 SDK version.

Article finding from our test: This SDK-vs-runtime distinction is the number-one thing that trips up first-time IIS deployers. The Hosting Bundle has no SDK. If you try dotnet new or dotnet publish without an SDK, you'll see No .NET SDKs were found.

Now create and publish a sample app:

Powershell
# Make a working folder New-Item -ItemType Directory -Path "C:\src\RaffSampleApp" -Force Set-Location "C:\src\RaffSampleApp" # Create a Razor Pages webapp targeting .NET 10 dotnet new webapp --framework net10.0 --name RaffSampleApp --output . # Publish to deployment folder (framework-dependent, win-x64) dotnet publish -c Release -o C:\publish\RaffSampleApp --runtime win-x64 --self-contained false

The first time you run any dotnet command after installing the SDK, you may see a welcome message and telemetry information. The template may also create an ASP.NET Core HTTPS development certificate. That development certificate is not the same as a production IIS certificate.

Path B — Build elsewhere, copy to server (production pattern)

This is the better production pattern. Build on a developer workstation, a CI/CD runner such as GitHub Actions, Azure DevOps, GitLab CI, or a dedicated build server. Then copy the published output to the Raff Server.

On your build machine:

Powershell
# From your project directory dotnet publish -c Release -o C:\publish\YourApp --runtime win-x64 --self-contained false

Then transfer the published folder contents to your Raff Server.

Options:

  • RDP file transfer — copy from your local machine and paste into the RDP session
  • robocopy over SMB if you have file-share access to the server
  • scp or SFTP if you have OpenSSH configured on the server
  • Git-based deploygit pull on the server, then publish locally if the SDK is installed
  • Object storage drop — upload from CI, then pull down on the server

Whichever method you use, the result is the same: your published files sit in a folder on the Windows Server, ready for IIS to point at.

Self-contained vs framework-dependent

We used:

Powershell
--self-contained false

This is a framework-dependent publish. Your app uses the .NET 10 runtime installed on the server via the Hosting Bundle.

For most IIS deployments, framework-dependent publishing is the right default because:

  • output is smaller
  • runtime patching is easier
  • multiple apps can share the installed runtime
  • it works naturally with the Hosting Bundle

The alternative is:

Powershell
dotnet publish -c Release -o C:\publish\YourApp --runtime win-x64 --self-contained true

A self-contained publish bundles the runtime with your app, making the output larger. It can be useful when you need to pin a specific runtime version per app, but for most Raff Windows VM deployments, framework-dependent publish plus the Hosting Bundle is simpler.

What gets published

After dotnet publish, the output folder contains files such as:

  • RaffSampleApp.exe — the launcher executable IIS will run
  • RaffSampleApp.dll — the app code
  • RaffSampleApp.deps.json and RaffSampleApp.runtimeconfig.json — runtime configuration
  • appsettings.json and environment-specific appsettings files
  • web.config — auto-generated; tells IIS how to launch the app via ANCM
  • wwwroot\ — static files such as CSS, JavaScript, and images
  • static web asset files created by the ASP.NET Core build

What's in the auto-generated web.config?

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=".\RaffSampleApp.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" /> </system.webServer> </location> </configuration>

Key things in there:

  • modules="AspNetCoreModuleV2" — the IIS handler installed by the Hosting Bundle
  • processPath=".\RaffSampleApp.exe" — IIS launches the app executable
  • hostingModel="inprocess" — the app runs inside the IIS worker process
  • stdoutLogEnabled="false" — stdout logs are disabled by default
  • <location path="." inheritInChildApplications="false"> — prevents this app's config from leaking into nested child apps

You do not need to edit this file for the sample deployment. For real production apps, you may temporarily enable stdout logging during troubleshooting, or add custom headers, MIME types, and security rules as needed.

Production note: expose IIS, not the app process

In this IIS setup, public traffic should reach IIS on port 80 or 443. Your ASP.NET Core app should not be exposed directly on a random Kestrel port.

The intended production path is:

Text
Public browser request IIS binding on 80/443 ASP.NET Core Module V2 ASP.NET Core app

Keep the app behind IIS and use IIS for bindings, HTTPS, request handling, logging, and process management.

Step 5 — Create the IIS site and app pool

Back on the Raff Server, create the IIS site:

Powershell
Import-Module WebAdministration # Stop and delete the default IIS site (we want our app on port 80) Stop-Website -Name "Default Web Site" -ErrorAction SilentlyContinue Remove-Website -Name "Default Web Site" -ErrorAction SilentlyContinue # Create the application pool New-WebAppPool -Name "RaffSampleAppPool" # Set No Managed Code (managedRuntimeVersion = "" is the setting for ASP.NET Core) Set-ItemProperty -Path "IIS:\AppPools\RaffSampleAppPool" -Name "managedRuntimeVersion" -Value "" # Create the IIS site pointing at our published folder New-Website -Name "RaffSampleApp" ` -PhysicalPath "C:\publish\RaffSampleApp" ` -ApplicationPool "RaffSampleAppPool" ` -Port 80 # Grant the app pool identity read access to the published folder icacls "C:\publish\RaffSampleApp" /grant "IIS AppPool\RaffSampleAppPool:(OI)(CI)RX"

Why "No Managed Code"?

ASP.NET Core does not run on the classic .NET Framework CLR inside IIS. It runs through ASP.NET Core Module V2 and the ASP.NET Core runtime.

Setting:

Powershell
managedRuntimeVersion = ""

tells IIS not to load the legacy .NET Framework CLR for this pool.

If you forget this, the app may fail with an HTTP 500.30 error when IIS tries to start it.

Permissions

icacls "..." /grant "IIS AppPool\RaffSampleAppPool:(OI)(CI)RX" grants the app pool's auto-generated virtual identity access to the published folder.

  • (OI) — Object Inherit, applies to files in the folder
  • (CI) — Container Inherit, applies to subfolders
  • RX — Read and Execute

If your app needs to write files such as logs, uploads, or generated reports, grant Modify access only to the specific folder where writes are required.

Example for a logs subfolder:

Powershell
New-Item -ItemType Directory -Path "C:\publish\RaffSampleApp\logs" -Force icacls "C:\publish\RaffSampleApp\logs" /grant "IIS AppPool\RaffSampleAppPool:(OI)(CI)M"

Do not grant Modify access to the whole site folder unless you have a clear reason.

Verify

Powershell
Get-Website -Name "RaffSampleApp" | Format-Table Name, State, PhysicalPath, Bindings Get-IISAppPool -Name "RaffSampleAppPool" | Format-Table Name, State, ManagedRuntimeVersion Invoke-WebRequest -Uri http://localhost -UseBasicParsing | Select-Object StatusCode, StatusDescription

Expected:

  • Site State: Started, PhysicalPath: C:\publish\RaffSampleApp
  • App pool State: Started
  • App pool ManagedRuntimeVersion is blank
  • HTTP StatusCode: 200

Now open Edge on the server and browse to http://localhost:

ASP.NET Core 10 Razor Pages welcome page on IIS — RaffSampleApp running on Windows Server 2025

You should see the RaffSampleApp welcome page with the navbar showing Home and Privacy links, the "Welcome" heading, and footer attribution.

This means the full pipeline is working:

Text
Browser → IIS → ASP.NET Core Module V2 → ASP.NET Core 10 → HTTP 200

Deploying this for a real app?
Use a clean Windows VM, configure IIS properly, verify the app through RDP and PowerShell, then add HTTPS, backups, and monitoring before production.

Raff Windows VMs are built for IIS hosting, ASP.NET Core apps, RDP administration, SQL Server workloads, and business software hosting.
View Raff Windows VM plans

Step 6 — Open the firewall for external access

So far we've only confirmed the app responds locally on the server. To reach it from your browser on another machine, open HTTP and HTTPS in Windows Firewall:

Powershell
# Open port 80 (HTTP) in Windows Firewall New-NetFirewallRule ` -DisplayName "HTTP (TCP 80)" ` -Direction Inbound ` -Protocol TCP ` -LocalPort 80 ` -Action Allow ` -Profile Any # Open port 443 (HTTPS) too, for when you set up a TLS certificate New-NetFirewallRule ` -DisplayName "HTTPS (TCP 443)" ` -Direction Inbound ` -Protocol TCP ` -LocalPort 443 ` -Action Allow ` -Profile Any

Now test from your local machine:

Text
http://YOUR_SERVER_IP

For a real production deployment, do not stop at raw HTTP access. Configure a domain, install a TLS certificate, redirect HTTP to HTTPS, and scope any admin-only ports or tools to trusted IPs.

At this point, your ASP.NET Core app is reachable from outside the server.

For production, the next steps are:

  • point a domain to the server
  • configure HTTPS
  • redirect HTTP to HTTPS
  • set up backups or snapshots
  • monitor uptime and disk usage
  • keep Windows Server and .NET patched

Common errors

The five errors you'll most likely encounter, with solutions.

HTTP Error 500.30 — ASP.NET Core app failed to start

This is the most common ASP.NET Core on IIS error.

Causes:

  1. App pool has the wrong managedRuntimeVersion. It should be empty (""), not v4.0.
  2. Hosting Bundle is missing or the wrong runtime version is installed.
  3. The app throws during startup. Common reasons include a missing connection string, bad appsettings.json, missing environment variables, or an unhandled exception in Program.cs.
  4. Permissions are incorrect. The app pool identity cannot read the published folder or write to a required logs/uploads folder.

First, confirm the app pool setting:

Powershell
Get-IISAppPool -Name "RaffSampleAppPool" | Select-Object Name, ManagedRuntimeVersion

If ManagedRuntimeVersion is not blank, fix it:

Powershell
Set-ItemProperty -Path "IIS:\AppPools\RaffSampleAppPool" -Name "managedRuntimeVersion" -Value ""

Then confirm the runtime:

Powershell
dotnet --list-runtimes

You should see Microsoft.AspNetCore.App 10.0.x.

To see the actual exception, temporarily enable stdout logging in web.config:

Xml
<aspNetCore processPath=".\YourApp.exe" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" hostingModel="inprocess"> </aspNetCore>

Create the logs folder and grant the app pool write access:

Powershell
New-Item -ItemType Directory -Path "C:\publish\YourApp\logs" -Force icacls "C:\publish\YourApp\logs" /grant "IIS AppPool\YourAppPool:(OI)(CI)M"

Restart IIS:

Powershell
iisreset

Reload the page, then read the generated stdout log:

Text
C:\publish\YourApp\logs\stdout_*.log

Disable stdout logging after diagnosis. It can slow the app and the log files can grow without rotation.

You can also check the Windows Event Log:

Powershell
Get-EventLog -LogName Application -Source "IIS AspNetCore Module*" -Newest 10 | Format-List TimeGenerated, Message

HTTP Error 500.19 — Internal Server Error / web.config error

This usually means IIS cannot read or interpret web.config.

Causes:

  • Missing ANCM module. Confirm with Get-WebGlobalModule -Name "AspNetCoreModuleV2".
  • Corrupt web.config. Validate the XML syntax or republish the app.
  • Wrong .NET version targeted. dotnet --list-runtimes does not include your target framework.
  • Incomplete publish output. The deployment folder is missing files from dotnet publish.

Check ANCM:

Powershell
Get-WebGlobalModule -Name "AspNetCoreModuleV2" | Format-Table Name, Image

If empty, repair or reinstall the Hosting Bundle.

HTTP Error 502.5 — Process Failure

The app process could not start. Diagnose it like 500.30:

  • confirm the runtime is installed
  • check app pool settings
  • enable stdout logging temporarily
  • review Windows Event Log
  • confirm the app can read its configuration and dependencies

This error is more common with older out-of-process hosting model deployments. Modern in-process ASP.NET Core apps often show 500.30 instead.

App works on http://localhost from the server but not externally

This is usually firewall, binding, or DNS.

Check Windows Firewall:

Powershell
Get-NetFirewallRule -DisplayName "HTTP (TCP 80)", "HTTPS (TCP 443)" | Format-Table DisplayName, Enabled, Direction, Action

Check IIS bindings:

Powershell
Get-WebBinding -Name "RaffSampleApp"

Also confirm you are testing the correct public IP address or domain.

Bindings conflict — "Cannot create site because port 80 is in use"

Either the default IIS site is still bound to port 80, or another site already has the same binding.

Solutions:

  • Stop or remove the conflicting site:
    Powershell
    Stop-Website "Default Web Site" Remove-Website "Default Web Site"
  • Use host headers so multiple sites share port 80 by hostname:
    Powershell
    New-Website -Name "App1" -PhysicalPath "C:\publish\App1" -ApplicationPool "App1Pool" -Port 80 -HostHeader "app1.example.com" New-Website -Name "App2" -PhysicalPath "C:\publish\App2" -ApplicationPool "App2Pool" -Port 80 -HostHeader "app2.example.com"
  • Use a different port for local or temporary testing.

Each app hostname needs a DNS record pointing to the server.

dotnet command is not recognized

If this happens right after installing the Hosting Bundle or SDK, refresh PATH:

Powershell
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")

Or close and reopen PowerShell.

Then test:

Powershell
dotnet --info

No .NET SDKs were found

This means you installed the Hosting Bundle but not the SDK.

The Hosting Bundle is enough to run ASP.NET Core apps on IIS. The SDK is required to create, build, and publish apps.

Install the SDK:

Powershell
winget install --id Microsoft.DotNet.SDK.10 --silent --accept-source-agreements --accept-package-agreements

Then verify:

Powershell
dotnet --list-sdks

Hosting multiple ASP.NET Core sites on one Raff Server

You can host multiple ASP.NET Core apps on one Raff Windows VM. Pattern:

  1. One app pool per site — each site gets its own IIS worker process. If one site crashes or hangs, the others are less likely to be affected.
  2. Each app pool: No Managed Code — use managedRuntimeVersion = "" for every ASP.NET Core app pool.
  3. Different host headers or ports per site — most production setups use host headers and DNS.
  4. Separate deployment folders — keep each app isolated.
  5. Separate permissions per app pool identity — only grant each app access to its own folder.
  6. Memory limits per pool — optionally recycle an app pool if a memory leak grows too large.

Example: limit RaffSampleAppPool to 2 GB private memory before recycle.

Powershell
Set-ItemProperty -Path "IIS:\AppPools\RaffSampleAppPool" ` -Name "recycling.periodicRestart.privateMemory" ` -Value 2097152

The value is in KB. 2097152 equals 2 GB.

Production-ready multi-site deployment deserves its own guide. The single-site pattern in this article is the foundation.

Production note: persist ASP.NET Core Data Protection keys

If your ASP.NET Core app uses authentication cookies, antiforgery tokens, password reset tokens, or other protected payloads, make sure ASP.NET Core Data Protection keys are persisted correctly.

On a single IIS server, the default setup may work. But production apps should still treat key storage as part of deployment planning.

This matters because losing or rotating keys unexpectedly can sign users out or invalidate protected tokens.

For multi-server deployments, shared key storage becomes more important because all app instances need access to the same key ring.

Why use a Windows VM for ASP.NET Core and IIS?

ASP.NET Core can run on Linux, containers, managed platforms, Kubernetes, or Windows Server. But IIS on a Windows VM is still a practical choice when your team wants direct control over the Windows hosting environment.

A Windows VM is a good fit when:

  • you want RDP access to manage the server
  • your team already knows IIS
  • you need to host ASP.NET Core and older Windows-based apps together
  • you want direct access to app pools, bindings, certificates, logs, and folders
  • you need to run SQL Server or other Windows software near the app
  • you prefer a predictable VM instead of a larger cloud platform setup

Raff Windows VMs are built for teams that want a simple Windows environment for IIS, ASP.NET Core, SQL Server, RDP, and business app hosting without hyperscaler complexity.

View Raff Windows VM plans

Verify deployment with PowerShell

After every deployment, run a quick smoke test to confirm IIS is healthy and your app is responding.

Save this as:

Text
C:\IIS\Verify-Site.ps1

Script:

Powershell
param([string]$SiteName = "RaffSampleApp", [string]$Url = "http://localhost") Write-Host "`n=== IIS Site Health Check: $SiteName ===" -ForegroundColor Cyan # IIS services $services = Get-Service -Name W3SVC, WAS $services | ForEach-Object { if ($_.Status -eq 'Running') { Write-Host "PASS: $($_.Name) running" -ForegroundColor Green } else { Write-Host "FAIL: $($_.Name) is $($_.Status)" -ForegroundColor Red } } # Site state $site = Get-Website -Name $SiteName -ErrorAction SilentlyContinue if (-not $site) { Write-Host "FAIL: site '$SiteName' not found" -ForegroundColor Red return } if ($site.State -eq 'Started') { Write-Host "PASS: site '$SiteName' is Started" -ForegroundColor Green } else { Write-Host "FAIL: site '$SiteName' is $($site.State)" -ForegroundColor Red } # App pool state $pool = Get-IISAppPool -Name $site.applicationPool -ErrorAction SilentlyContinue if ($pool.State -eq 'Started') { Write-Host "PASS: app pool '$($pool.Name)' is Started" -ForegroundColor Green } else { Write-Host "FAIL: app pool '$($pool.Name)' is $($pool.State)" -ForegroundColor Red } # No Managed Code check if ($pool.ManagedRuntimeVersion -eq '') { Write-Host "PASS: app pool managedRuntimeVersion is empty (No Managed Code, correct for ASP.NET Core)" -ForegroundColor Green } else { Write-Host "FAIL: app pool managedRuntimeVersion is '$($pool.ManagedRuntimeVersion)' (should be empty for ASP.NET Core)" -ForegroundColor Red } # HTTP test try { $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop if ($response.StatusCode -eq 200) { Write-Host "PASS: $Url responds with HTTP 200 OK" -ForegroundColor Green } else { Write-Host "WARN: $Url responds with HTTP $($response.StatusCode)" -ForegroundColor Yellow } } catch { Write-Host "FAIL: $Url is not responding — $($_.Exception.Message)" -ForegroundColor Red } # Recent error events (last 1 hour) $events = Get-EventLog -LogName Application -Source "IIS AspNetCore Module*" -After (Get-Date).AddHours(-1) -ErrorAction SilentlyContinue if ($events) { Write-Host "WARN: $($events.Count) ANCM event(s) in last hour:" -ForegroundColor Yellow $events | Select-Object -First 3 | Format-List TimeGenerated, EntryType, Message } else { Write-Host "PASS: no recent ANCM errors" -ForegroundColor Green } Write-Host "`n=== Health check complete ===" -ForegroundColor Cyan

Run after every deployment:

Powershell
.\Verify-Site.ps1 -SiteName "RaffSampleApp" -Url "http://localhost"

Every check should show PASS. Any FAIL or WARN is something to investigate before declaring the deployment done.

Basic production checklist

Before you call the deployment production-ready, check these items:

  • IIS is installed and running.
  • The .NET 10 Hosting Bundle is installed.
  • AspNetCoreModuleV2 is registered with IIS.
  • The app pool uses No Managed Code.
  • The app has its own IIS site and app pool.
  • The app pool identity has only the permissions it needs.
  • Public traffic reaches IIS on port 80 or 443, not a direct Kestrel port.
  • HTTP and HTTPS firewall rules are correct.
  • A domain points to the server.
  • HTTPS is configured.
  • HTTP redirects to HTTPS.
  • The app is not running in the Development environment.
  • Secrets are not stored directly in source code.
  • Logs are written to a controlled location.
  • ASP.NET Core Data Protection keys are persisted appropriately.
  • Backups or snapshots are configured.
  • Windows Server and .NET runtime patching are planned.
  • Monitoring is configured for uptime, disk usage, and app health.

Tested on Raff

Tested on: ASP.NET Core 10 Razor Pages app on IIS 10 — Microsoft .NET 10 Hosting Bundle 10.0.7, Microsoft .NET SDK 10.0.203 — on a Raff Windows Server 2025 VM — Production plan, 4 vCPU / 8 GB RAM / 120 GB NVMe, Windows Server 2025 Standard build 26100, Vint Hill, Virginia datacenter — on April 30, 2026, by Serdar Tekin. We verified end-to-end on a fresh VM: IIS install via Install-WindowsFeature with 19 features; .NET 10 Hosting Bundle install via winget; ASP.NET Core Module V2 registration with IIS; .NET 10 SDK install via winget; dotnet new webapp --framework net10.0 Razor Pages template; framework-dependent publish to C:\publish\RaffSampleApp; auto-generated web.config using processPath=".\RaffSampleApp.exe" and hostingModel="inprocess"; default site removal and replacement with the RaffSampleApp site on port 80; app pool RaffSampleAppPool with managedRuntimeVersion=""; app pool identity granted RX permissions on the published folder via icacls; HTTP 200 OK confirmed via both Invoke-WebRequest and Edge browser load of the Razor Pages welcome page. Common-error scenarios are documented from Microsoft Learn references rather than live-tested in this session.

What's next

If you are preparing a production ASP.NET Core deployment on Windows Server, these guides are the natural next steps:

FAQ

Do I need the .NET 10 Hosting Bundle to host ASP.NET Core on IIS?

Yes. For IIS hosting, install the .NET 10 Hosting Bundle on the Windows Server. It installs the runtime components and ASP.NET Core Module V2 required for IIS to run ASP.NET Core apps.

Is the .NET 10 Hosting Bundle the same as the .NET SDK?

No. The Hosting Bundle is for running ASP.NET Core apps on IIS. The SDK is for creating, building, and publishing apps with commands like dotnet new and dotnet publish.

Why does the IIS app pool need No Managed Code?

ASP.NET Core does not run through the classic .NET Framework CLR app pool runtime. Set the app pool managed runtime version to blank, also shown in IIS Manager as No Managed Code.

Can I host multiple ASP.NET Core apps on one Windows VM?

Yes. Use one IIS site and one app pool per app. Use host headers to route different domains to different apps on the same server.

Can I use Windows Server 2022 instead of Windows Server 2025?

Yes. The same IIS and ASP.NET Core hosting pattern works on supported Windows Server versions. The exact UI and installed feature behavior may vary slightly.

Should I build the app directly on the production server?

For quick tests, it is fine. For production, build on your local machine or CI/CD system, then copy the published output to the server. This keeps build tools off the production VM.

Why does my ASP.NET Core app show HTTP Error 500.30?

Usually because the app failed to start. Check the app pool setting, installed runtimes, app permissions, connection strings, environment variables, and stdout logs.

Can I host ASP.NET Core on Linux instead?

Yes. ASP.NET Core runs well on Linux with Kestrel behind NGINX or another reverse proxy. This guide is specifically for teams that want Windows Server and IIS.

Sources


Microsoft, Windows Server, IIS, .NET, ASP.NET Core, and Edge are trademarks of Microsoft Corporation. Raff Technologies is an independent infrastructure provider and is not affiliated with, sponsored by, or endorsed by Microsoft Corporation.

Was this article helpful?

Published April 30, 2026 · Updated April 30, 2026

Back to hub
Ready when you are

Your Windows Server, live in ~55 seconds

Genuine, fully-licensed Windows with full admin and RDP. We handle the Microsoft licensing, billed monthly with nothing upfront. On NVMe SSD, backed by a 14-day money-back guarantee.

Related articles