How to Deploy ASP.NET Applications on Windows Server 2022
Windows Server 2022 with IIS is the standard hosting platform for both classic ASP.NET (Framework) applications and modern ASP.NET Core applications. The deployment process, application pool configuration, and hosting model differ significantly between the two. This guide covers both paths in detail — from deploying a .NET Framework 4.8 WebForms or MVC application via Web Deploy, through to hosting a .NET 8 ASP.NET Core app with the in-process hosting model and configuring structured logging with Serilog.
Prerequisites: IIS and ASP.NET Features
Before deploying any ASP.NET application, ensure IIS and the required ASP.NET features are installed. Run the following in an elevated PowerShell session:
# Install IIS with common features
Install-WindowsFeature -Name Web-Server -IncludeManagementTools
# Install ASP.NET 4.8 support
Install-WindowsFeature -Name Web-Asp-Net45
# Install ASP.NET Core Hosting Bundle (for .NET Core/5+)
# Download the latest Hosting Bundle from Microsoft first
# Then install silently:
Start-Process -FilePath "dotnet-hosting-8.0.0-win.exe" `
-ArgumentList "/install /quiet /norestart" -Wait
# Verify the ASP.NET Core Module (ANCM) is installed
Get-WebConfiguration system.webServer/globalModules | Where-Object { $_.Name -like "*AspNetCore*" }
The ASP.NET Core Hosting Bundle installs the .NET runtime, the ASP.NET Core runtime, and the ASP.NET Core IIS Module (ANCM v2) in one package. It is mandatory for hosting ASP.NET Core applications in IIS.
Deploying ASP.NET Framework Applications to IIS
Classic ASP.NET (WebForms, MVC 5, Web API 2) applications run on .NET Framework 4.x and are deployed to IIS using either Web Deploy (MSDeploy) or simple file copy (xcopy deploy).
xcopy / robocopy deployment:
# Create the web root directory
New-Item -Path "C:inetpubwwwrootMyApp" -ItemType Directory -Force
# Copy compiled output (bin, Views, Content, etc.)
robocopy "C:BuildMyApppublish" "C:inetpubwwwrootMyApp" /E /XO /NFL /NDL
# Set IIS_IUSRS permissions on the web root
icacls "C:inetpubwwwrootMyApp" /grant "IIS_IUSRS:(OI)(CI)RX" /T
Web Deploy (MSDeploy) deployment:
Install Web Deploy 3.6 on the server from the IIS Web Platform Installer or the direct download. Then from your build machine or pipeline:
# Deploy a Web Deploy package (.zip) to IIS
msdeploy.exe -verb:sync `
-source:package="C:BuildMyApp.zip" `
-dest:auto,computerName="https://myserver:8172/msdeploy.axd?site=MyApp",`
authType="Basic",userName="deployuser",password="DeployP@ssword!" `
-setParam:name="IIS Web Application Name",value="Default Web Site/MyApp" `
-allowUntrusted
ASP.NET Application Pools
Application pools isolate web applications from each other. For ASP.NET Framework apps, the pool must use the Classic or Integrated pipeline mode. For ASP.NET Core, the app pool must use No Managed Code because the .NET runtime is hosted inside the application process, not IIS.
Import-Module WebAdministration
# Create an application pool for ASP.NET Framework 4.8
New-WebAppPool -Name "MyFrameworkApp"
Set-ItemProperty -Path "IIS:AppPoolsMyFrameworkApp" -Name "managedRuntimeVersion" -Value "v4.0"
Set-ItemProperty -Path "IIS:AppPoolsMyFrameworkApp" -Name "managedPipelineMode" -Value "Integrated"
# Create an application pool for ASP.NET Core (No Managed Code)
New-WebAppPool -Name "MyCoreApp"
Set-ItemProperty -Path "IIS:AppPoolsMyCoreApp" -Name "managedRuntimeVersion" -Value ""
# Run the pool as a specific service account (recommended over ApplicationPoolIdentity for AD environments)
Set-ItemProperty -Path "IIS:AppPoolsMyCoreApp" -Name "processModel.userName" -Value "DOMAINsvc-webapp"
Set-ItemProperty -Path "IIS:AppPoolsMyCoreApp" -Name "processModel.password" -Value "ServiceAccount@Pass"
Set-ItemProperty -Path "IIS:AppPoolsMyCoreApp" -Name "processModel.identityType" -Value 3
Configuring Session State (ASP.NET Framework)
ASP.NET Framework supports multiple session state providers: InProc (default), SQL Server, and distributed cache. InProc session is lost when the app pool recycles; for production use SQL Server session state or a Redis provider.
Initialize the ASPState database on SQL Server using the aspnet_regsql.exe tool:
C:WindowsMicrosoft.NETFramework64v4.0.30319aspnet_regsql.exe `
-S sql01 -E -ssadd -sstype p
Connection Strings in web.config
Connection strings for ASP.NET Framework apps are typically stored in web.config. Sensitive values should be encrypted using the ASP.NET Configuration Protection API or moved to environment variables / Azure Key Vault in modern deployments.
To encrypt the connectionStrings section on the server:
C:WindowsMicrosoft.NETFramework64v4.0.30319aspnet_regiis.exe `
-pe "connectionStrings" `
-app "/MyApp" `
-prov "DataProtectionConfigurationProvider"
Deploying ASP.NET Core: Framework-Dependent vs Self-Contained
When publishing an ASP.NET Core application, you choose between two deployment models. Framework-dependent deployment (FDD) produces a small package and requires the .NET runtime to be installed on the server. Self-contained deployment (SCD) bundles the runtime with the application and has no external dependencies.
# Framework-dependent publish (requires .NET 8 runtime on IIS server)
dotnet publish MyWebApp.csproj `
-c Release `
-r win-x64 `
--no-self-contained `
-o C:PublishedMyWebApp
# Self-contained publish (no runtime required on server)
dotnet publish MyWebApp.csproj `
-c Release `
-r win-x64 `
--self-contained true `
-p:PublishSingleFile=false `
-o C:PublishedMyWebApp-SCD
In-Process vs Out-of-Process Hosting
ASP.NET Core on IIS supports two hosting models controlled by the hostingModel attribute in web.config:
- In-process (default): The .NET Core runtime is hosted inside the IIS worker process (w3wp.exe). Higher performance, lower latency. Recommended for most scenarios.
- Out-of-process: IIS acts as a reverse proxy forwarding requests to Kestrel running as a separate process. More isolated; the app can be restarted independently.
For out-of-process hosting, change hostingModel="outofprocess" and the processPath should point to the application executable for self-contained deploys, or remain as dotnet for FDD.
Environment-Specific Transforms (web.Release.config)
Web.config transformation (XDT) allows you to maintain a base web.config and apply environment-specific changes during deployment. Create web.Release.config in the same directory as web.config:
Apply the transform manually using the Microsoft.Web.XmlTransform library or via msbuild:
msbuild MyWebApp.csproj /T:TransformWebConfig /P:Configuration=Release
Health Checks in ASP.NET Core
ASP.NET Core has a built-in health check middleware. Register it in Program.cs and expose an endpoint that load balancers and monitoring tools can poll:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks()
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("DefaultConnection"),
healthQuery: "SELECT 1",
name: "sql-server",
failureStatus: HealthStatus.Degraded,
tags: new[] { "db", "sql", "sqlserver" }
);
var app = builder.Build();
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
app.Run();
Logging with Serilog and NLog in ASP.NET Core
ASP.NET Core’s built-in logging is extensible. Serilog and NLog are the most widely used third-party providers. Install Serilog with IIS and file sinks:
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.File
dotnet add package Serilog.Sinks.EventLog
Configure Serilog in Program.cs:
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File(
path: @"C:logsmyappapp-.log",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
)
.WriteTo.EventLog("MyWebApp", manageEventSource: true)
.CreateLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
var app = builder.Build();
// ... configure middleware
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application start-up failed");
}
finally
{
Log.CloseAndFlush();
}
For NLog, install NLog.Web.AspNetCore and place an nlog.config file in the application root:
Enable NLog in Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Host.UseNLog();
Creating the IIS Site and Binding
Once the application is deployed and the app pool is configured, create the IIS website binding:
Import-Module WebAdministration
# Create the website
New-Website -Name "MyWebApp" `
-PhysicalPath "C:inetpubwwwrootMyWebApp" `
-ApplicationPool "MyCoreApp" `
-Port 443 `
-Ssl
# Bind a TLS certificate (replace thumbprint with your cert's thumbprint)
$cert = Get-ChildItem -Path Cert:LocalMachineMy | Where-Object { $_.Subject -match "myapp.example.com" }
New-WebBinding -Name "MyWebApp" -Protocol "https" -Port 443 -HostHeader "myapp.example.com" -SslFlags 1
(Get-WebBinding -Name "MyWebApp" -Protocol "https").AddSslCertificate($cert.Thumbprint, "My")