blog post image
Andrew Lock avatar

Andrew Lock

~8 min read

Running an ASP.NET Core app inside IIS in a Windows container

Share on:

In this post I first discuss the differences between Linux and Windows containers, and then describe how to run an ASP.NET Core app inside IIS, inside of a Windows container. I show how to install the AspNetCoreModule for running ASP.NET Core in IIS, how to use APPCMD and the PowerShell snippets to create app pools and websites, and how to use ServiceMonitor.exe to watch an app pool. Finally, I describe how I resolved a couple of issues I ran into running an ASP.NET Core app in IIS in a container.

Running apps in containers

I'm a big fan of using containers to build and package applications. You can use containers to somewhat declaratively define your application's dependencies, and how to build it, and then when you run your application you know you will get the same behaviour wherever you run it. As long as the host has docker (or a comparable container runtime) then you can build and run your application.

Pretty much, anyway. Once you have built your application it's set in stone, but you're still vulnerable when you build your image to things like download links disappearing and dependencies no longer being available. But at least those dependencies are generally explicit in your dockerfile.

Of course, when people say "containers", they're normally talking about Linux containers. Linux containers really give the best experience for both building and running ASP.NET Core applications, but there also Windows containers.

Windows containers are conceptually similar Linux containers; they allow a similar "mini-VM" experience, in which the container is isolated from the host but the operating-system kernel is re-used. In practical terms though, they feel very differnt.

  • You can run virtually any Linux container that supports your process architecture (generally x64 or arm64) on any host. Windows, Linux, and macOS can all run Linux containers. However the base image of Windows containers must match the host. So you can only run Windows Server 2022 containers on a Windows Server 2022 machine, not a 2019 machine (for example).
  • Windows images are much bigger than their Linux counterparts. We're talking 10-1000×—instead of a few MB for alpine you're looking at a few GB for a Windows Server Core image.
  • Predominantly due to the sheer size, Windows containers feel a lot slower to work with than Linux containers in general.
  • Some of the authoring experiences in Windows dockerfiles feels cumbersome compared to writing Linux dockerfiles.

So, given all that, why would you choose Windows containers? To my mind, the main answer is generally "because you have to". Windows containers still have the advantage of defining an isolated environment compared to running on a host directly. And in some cases you need to run on Windows.

Maybe you have an app that explicitly relies on Windows-only APIs to run. Perhaps you're deploying an ASP.NET (non-core) app and need to run it in IIS. Or maybe you are running an ASP.NET Core app, but you need to run it on Windows, or even inside IIS.

Running an ASP.NET Core application, inside IIS, in windows containers

In this section, I describe a dockerfile that builds an ASP.NET Core application, and then hosts it in a windows dockerfile, running in-process in IIS. The example I use is a simple .NET 9 ASP.NET Core app. The details aren't important, I'm more interested in how to prepare the image itself.

The image below is a multi-stage build. In the first stage we create and publish our .NET web app. In the second stage we do all of the following:

  • Use an IIS/ASP.NET base image
  • Switch to using PowerShell for scripting instead of cmd (optional)
  • Download and install the 9.0 ASP.NET Core hosting bundle (for IIS integration)
  • Copiy the build assets
  • Create a new app pool and app site to host the ASP.NET Core app
# Build the ASP.NET Core app using the latest SDK
FROM mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 AS builder

# Build the test app
WORKDIR /src
RUN dotnet new web --name AspNetCoreTest --output .
RUN dotnet publish "AspNetCoreTest.csproj" -c Release -o /src/publish

# There are other runtime images you could use - this image includes IIS and ASP.NET 
# If you don't need aspnet or .NET Framework (we don't in this example), you can
# probably use mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2022
FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8-windowsservercore-ltsc2022 AS publish
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"]

WORKDIR /app

# Install the hosting bundle
RUN  $url='https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.0/dotnet-hosting-9.0.0-win.exe'; \
    echo "Fetching " + $url; \
    Invoke-WebRequest $url -OutFile c:/hosting.exe; \
    Start-Process -Wait -PassThru -FilePath "c:/hosting.exe" -ArgumentList @('/install', '/q', '/norestart'); \
    rm c:/hosting.exe;

# Copy the app across
COPY --from=builder /src/publish /app/.

# Create new website we control, and a new app pool set to "No Managed Code"
RUN Remove-WebSite -Name 'Default Web Site'; \
    c:\Windows\System32\inetsrv\appcmd add apppool /name:AspNetCorePool /managedRuntimeVersion:""; \
    New-Website -Name 'SmokeTest' -Port 5000 -PhysicalPath 'c:\app' -ApplicationPool 'AspNetCorePool';

ENTRYPOINT ["C:\\ServiceMonitor.exe", "w3svc", "AspNetCorePool"]

The first interesting point in the above file is that we need to download and install the ASP.NET Core hosting bundle. This includes the ASP.NET Core runtime, but also the ASP.NET Core Module (ANCM) for IIS. The ANCM module provides integration between IIS and an ASP.NET Core app and is required to run ASP.NET Core apps in IIS.

The second interesting point is using the IIS Administration tools to manage websites and app pools. The example above uses two different approaches (mostly for effect, not due to necessity): appcmd.exe and IISAdministration PowerShell Cmdlets.

Note that the PowerShell Cmdlets were introduced in IIS 10.0, which shipped with Windows Server 2016.

Finally, you can see that we have defined an entrypoint that invokes ServiceMonitor.exe. This overrides the entrypoint that is defined in the base aspnet image, which similarly invokes ServiceMonitor.exe. ServiceMonitor.exe is a Windows executable designed to be used as the entrypoint process when running IIS inside a Windows Server container. It's available by default in the base images.

As described in the project's README:

ServiceMonitor monitors the status of the w3svc service and will exit when the service state changes from SERVICE_RUNNING to either one of SERVICE_STOPPED, SERVICE_STOP_PENDING, SERVICE_PAUSED or SERVICE_PAUSE_PENDING.

Additionally, ServiceMonitor will promote environment variables from process environment it's own process environment block to the DefaultAppPool. We achieve this by naively copying all variables in our process environment block except for those Environment variable / value pairs present in this list below.

We override the entrypoint in this case to add the AspNetCorePool argument, so that the entrypoint monitors our new app pool, instead of the default pool.

We can build and run the app using something like the following:

docker build -t aspnetcore-iis-image
docker run -p 5000:5000 aspnetcore-iis-image

ServiceMonitor.exe watches the status of the AspNetCorePool app pool, and keeps the container running as long as that pool is running. If the pool shuts down, then the container also exits.

Controlling app pool startup in the container

I was working on a "smoke" test recently that uses a similar setup to that described above. The test app in question starts a background worker that makes a request to itself, and then shuts down the application. The intention in this case is that no external requests are required. However, this proved somewhat problematic to implement when hosting in IIS.

I specifically wanted the following behaviour:

  • When the container starts, the app pool is not yet started.
  • The app pool is then started and a worker process is created.
  • The ASP.NET Core application runs, sends a request to itself, and then exits.
  • The container exits.

Unfortunately, I struggled to convince IIS (and ServiceMonitor.exe) to do what I wanted.

The major problem I ran into is how IIS manages app pools. There are various properties that control how and when an app pool starts. There are also properties to control when a worker process is spun up to handle a request. Among these knobs are:

  • Start application pool immediately (AKA /autoStart)
    • Enabled/true: IIS will automatically start the application pool.
    • Disabled/false: You need to automatically start the application pool.
  • Worker process start mode (AKA /startMode)
    • AlwaysRunning: If the application pool is running, immediately start up the worker process.
    • OnDemand: If the application pool is running, start the w3wp.exe process when there is an inbound application request.
  • Preload the app pool: (AKA applicationDefaults.preloadEnabled)
    • True: IIS simulates a user request to the default page of an application or virtual directory so that it is initialized. The application pool's startMode setting must be set to AlwaysRunning.
    • False: The app pool is initialized when the first request is received.

So, in theory I thought I could do the following

  • Create an app pool with /autoStart="false" and /startMode="AlwaysRunning"
  • Create a site with applicationDefaults.preloadEnabled="true"
  • Run ServiceMonitor.exe to monitor the app pool. This would serve two purposes
    • Promote the ambient environment variables
    • Exit when the app pool shuts down

Unfortunately this doesn't work as I had hoped. As far as I could tell, the ASP.NET Core app wasn't starting up and then the app pool wasn't exiting when the app exits. I'm not entirely sure what's going on here, but I subsequently discovered there's also the applicationinitialization.doAppInitAfterRestart element and I realised I just didn't have the patience to fight with IIS any more 😅

In the end I opted for creating an entrypoint script that manually makes an http request to the app, explicitly stops the app pool, and then exits. It wasn't as smooth as I'm sure it could be, but at least it worked:

# We override the normal service monitor entrypoint, because we want the container to shut down after the request is sent
# - Run ServiceMonitor.exe to copy the env vars to the app pool
# - Explicitly start the app pool
# - Make a request (doesn't have to hit a "real" endpoint)
# - Stop the app pool (this should happen automatically, but just to be safe)
# - Exit
RUN echo 'Write-Host \"Running servicemonitor to copy environment variables\"; Start-Process -NoNewWindow -PassThru -FilePath \"c:/ServiceMonitor.exe\" -ArgumentList @(\"w3svc\", \"AspNetCorePool\");' > C:\app\entrypoint.ps1; \
    echo 'Write-Host \"Starting AspNetCorePool app pool\"; Start-WebAppPool -Name \"AspNetCorePool\" -PassThru;' >> C:\app\entrypoint.ps1; \
    echo 'Write-Host \"Making 404 request\"; curl http://localhost:5000;' >> C:\app\entrypoint.ps1; \
    echo 'Write-Host \"Stopping pool\";Stop-WebAppPool \"AspNetCorePool\" -PassThru;' >> C:\app\entrypoint.ps1;  \
    echo 'Write-Host \"Shutting down\"' >> C:\app\entrypoint.ps1;

# Set the script as the entrypoint
ENTRYPOINT ["powershell", "-File", "C:\\app\\entrypoint.ps1"]

And with that, I achieved what I wanted—an ASP.NET Core app, running in IIS, in a Windows container, which starts up and exits. There was one final strange hiccup I ran into though, which I'll cover in the final section.

Troubleshooting "APPCMD failed with error code 183"

While initially working with ServiceMonitor.exe, I kept running into this error:

Service 'w3svc' has been stopped

APPCMD failed with error code 183

Failed to update IIS configuration

Error code 183 is "Cannot create file when that file already exists". Err…OK.

Well, it turns out you get this error if you an environment variable explicitly set in the <environmentVariables> section and then also set in the docker environment. ServiceMonitor.exe naively tries to "promote" the ambient environment variables by calling "add variable". And if there's already a variable in the app pool, it throws the above exception. The only solution is to not double up—remove the environment variable either from the app pool or from the dockerfile.

Summary

In this post I described how I ran an ASP.NET Core app inside IIS, inside of a Windows container. I showed how to install the AspNetCoreModule for running ASP.NET Core in IIS, how to use APPCMD and the PowerShell snippets to create app pools and websites, and how to use ServiceMonitor.exe to watch an app pool. I then described a specific scenario I was trying to reproduce, as well as how to solve an error I ran into.

  • Buy Me A Coffee
  • Donate with PayPal
Andrew Lock | .Net Escapades
Want an email when
there's new posts?