Containerize This: Microsoft ASP.NET Containers
Continuing with the Containerize This! Series, Cloud Architect, Mike Zazon, looks at common web application technologies and how they can be used efficiently within Docker containers.
Do you have existing ASP.NET applications running on Windows Server? Are you tired of configuring IIS, bindings, and ready to modernize your existing IIS configuration management strategies? Are you hearing Docker and Kubernetes can solve all of your development, scaling, security, cost and operations problems? Would you like to get in on the container craze running classic .NET on IIS and Windows Server? If you answered yes to any of the above, this article is for you!
Microsoft .NET applications are ubiquitous across many sectors and they power some of the largest companies in the world. Microsoft has made great strides with broadening and evolving the .NET framework by introducing the cross-platform .NET core which lets .NET applications run inside of very lean linux containers. However many legacy applications won’t make the move to .NET core for a variety of reasons. You can now package classic ASP.NET applications in Docker containers just like everyone else running linux-based workloads and reap the same benefits as part of that strategy. Most popular container orchestrators support running Windows containers, so there’s no need to worry, you currently have a lot of choices for running containerized Microsoft applications and the options are expanding by the day.
Containers allow organizations to move to PaaS-based application hosting on a portable platform of choice. For example, existing on-premise Windows Server infrastructure can be repurposed and reconfigured to host container-based applications. This portability paves a path for a seamless move to public cloud container hosting platforms like Amazon’s Elastic Container Service (ECS), Elastic Kubernetes Service (EKS), Azure Kubernetes Service (AKS), Azure Service Fabric, or the cloud-agnostic Docker Enterprise Edition.
Regardless of where you choose to run your containers, when you choose this strategy you minimize potential vendor lock-in from the hosting perspective. This gives your organization the ability to make decisions which provide agility by not tying your applications to a single vendor for hosting requirements.
Sample Application and Base Images
I have provideda repository on GitHub that you can follow along with. You can clone, build, and run this application with a few simple commands that we’ll walk through. We’re using a sample ASP.NET web application sourced originally from Microsoft’s ASP.NET sample repository. We’ve stripped out the unnecessary bits, upgraded a few packages and simplified the sample app targeting .NET 4.7.2. If you follow along with the simple instructions for this example, with a few commands you’ll be building and running a sample ASP.NET application in a Docker container.
In this example, we’re going to use a multi-stage Dockerfile to build the application using "nuget restore" and "msbuild" within the context of a "build" stage, and in the second stage copy the built artifacts into a runtime container image. Compiling with Visual Studio is not necessary for this example, the application builds from source code in the first stage of the Dockerfile. This example was built on the latest Windows Server 2019, but you should be able to use Windows 10 using Docker for Windows. Just make sure you’re using Windows containers!
The base images used are official Microsoft docker images hosted on Docker Hub. These images are routinely updated for security and compatibility purposes. Integrating this into your CI/CD strategy ensures you’re always running the latest upstream images and patched for security vulnerabilities as often as possible.
We mentioned previously that we’re using multi-stage builds to build the application from source using nuget restore and msbuild. In previous blog posts, we have identified this as one of the best things you can do to ensure your final built image will be as small as possible, without incorporating unnecessary binaries like SDKs and supporting tooling. It also allows you to automate builds very easily using your CI/CD Pipelines when you define your build and test environments as code in Dockerfiles. For example, this could be used in AWS CodePipeline, Azure DevOps Pipeline, or a Jenkins pipeline, with the final step being a push to the image repository of your choice. Amazon ECR, Azure ACR, and Docker Trusted Registry are great choices of repositories.
FROM microsoft/dotnet-framework:4.7.2-sdk AS build WORKDIR /app # copy csproj and restore as distinct layers COPY *.sln . COPY aspnetapp/*.csproj ./aspnetapp/ COPY aspnetapp/*.config ./aspnetapp/ RUN nuget restore # copy everything else and build app COPY aspnetapp/. ./aspnetapp/ WORKDIR /app/aspnetapp RUN msbuild /p:Configuration=Release # discard build environment and copy artifacts to runtime environment FROM mcr.microsoft.com/dotnet/framework/aspnet:4.7.2 AS runtime WORKDIR /inetpub/wwwroot COPY --from=build /app/aspnetapp/. ./ RUN powershell.exe -Command " \ Import-Module IISAdministration; \ $cert = New-SelfSignedCertificate -DnsName demo.cloudreach.internal -CertStoreLocation cert:\LocalMachine\My; \ $certHash = $cert.GetCertHash(); \ $sm = Get-IISServerManager; \ $sm.Sites[\"Default Web Site\"].Bindings.Add(\"*:443:\", $certHash, \"My\", \"0\"); \ $sm.CommitChanges();" EXPOSE 443
Configuring IIS via the Dockerfile
There are generally IIS configuration settings that need to be applied when running applications in production environments. Using some Powershell commands in the Dockerfile accomplishes these configuration needs as part of the build. This means the configuration is code and can be version controlled, tested, and repeated easily. For example, a common scenario you might have to address is injecting an SSL certificate into the IIS certificate store and attach it to an HTTPS binding for the application.
To demonstrate this, a self-signed certificate is generated at build time and a binding for IIS is added on port 443 to run the application in a single Powershell command. The RUN command on line 19 of the Dockerfile shows how to wrap this command together to achieve a single layer in the image. The final line of EXPOSE 443 allows a host port to be mapped to the container port to access the application over HTTPS. Note that in production scenarios you would likely be incorporating a real certificate from your PKI or trusted third party to accomplish this.
Build and Run!
When "docker build" is run, the docker daemon inspects the Dockerfile and process it to produce a docker image. Each line of the Dockerfile corresponds to a "layer" in the Docker image. Docker provides a powerful cache that we have explored in the past to speed up application builds, so being mindful of grouping commands and ordering Dockerfile commands to make sure you are taking advantage of this functionality is important and can really speed up your builds and tests. In general, things that change often should be placed lower in the Dockerfile, while things that change less often should be moved towards the top.
In the console output below, some verbose output has been omitted for display purposes, so don’t be alarmed when your own console scrolls quite a bit with build output when you run this yourself, it’s quite normal! The "nuget restore" and "msbuild" output is helpful to have when you’re getting started, to understand what is going on within each layer of the docker build.
PS C:\Users\Administrator\Desktop\aspnet-containerized> docker build --pull -t aspnetapp . Sending build context to Docker daemon 1.718MB Step 1/13 : FROM microsoft/dotnet-framework:4.7.2-sdk AS build 4.7.2-sdk: Pulling from microsoft/dotnet-framework 65014b3c3121: Already exists 9e2f2b17be72: Already exists e583fcdc4adc: Already exists 9870ac56a9c6: Pull complete Digest: sha256:590e9ddadfdac37e9c2b58f2b33ebbbd4393e8102ec0da783add291ba4ace511 Status: Downloaded newer image for microsoft/dotnet-framework:4.7.2-sdk ---> 1f9f0521b131 Step 2/13 : WORKDIR /app ---> Running in 98cc90b9f18d Removing intermediate container 98cc90b9f18d ---> 4147b08d4b91 Step 3/13 : COPY *.sln . ---> 5de8e2c1a9fc Step 4/13 : COPY aspnetapp/*.csproj ./aspnetapp/ ---> 4a581c162a8d Step 5/13 : COPY aspnetapp/*.config ./aspnetapp/ ---> d8907420bc30 Step 6/13 : RUN nuget restore ---> Running in 727b8fc48d3c Installed: 25 package(s) to packages.config projects Removing intermediate container 727b8fc48d3c ---> 98a072d1c02d Step 7/13 : COPY aspnetapp/. ./aspnetapp/ ---> 0236cfe1993c Step 8/13 : WORKDIR /app/aspnetapp ---> Running in ce5ad2bae874 Removing intermediate container ce5ad2bae874 ---> c16dd8b9ed4a Step 9/13 : RUN msbuild /p:Configuration=Release ---> Running in d4248ba054e1 Microsoft (R) Build Engine version 15.9.21+g9802d43bc3 for .NET Framework Copyright (C) Microsoft Corporation. All rights reserved. Build started 3/6/2019 4:23:00 PM. Done Building Project "C:\app\aspnetapp\aspnetapp.csproj" (default targets). Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:05.28 Removing intermediate container d4248ba054e1 ---> fc0ebee6c12d Step 10/13 : FROM mcr.microsoft.com/dotnet/framework/aspnet:4.7.2 AS runtime 4.7.2: Pulling from dotnet/framework/aspnet Digest: sha256:958114016f74f4ffc10b7f065ca4f340e0a0c390cb276c3659d5f8af43d388c7 Status: Downloaded newer image for mcr.microsoft.com/dotnet/framework/aspnet:4.7.2 ---> c231abd0f40f Step 11/13 : WORKDIR /inetpub/wwwroot ---> Running in 545958a64f0a Removing intermediate container 545958a64f0a ---> aad0f811675f Step 12/13 : COPY --from=build /app/aspnetapp/. ./ ---> 2f66925575d3 Step 13/13 : RUN powershell.exe -Command " Import-Module IISAdministration; $cert = New-SelfSignedCertificate -DnsName demo.cloudreach.internal -CertStoreLocation cert:\LocalMachine\My; $certHash = $cert.GetCertHash(); $sm = Get-IISServerManager; $sm.Sites[\"Default Web Site\"].Bindings.Add(\"*:443:\", $certHash, \"My\", \"0\"); $sm.CommitChanges();" ---> Running in 310324e83afc protocol bindingInformation sslFlags -------- ------------------ -------- https *:443: None Removing intermediate container 310324e83afc ---> 62a0abaa342a Successfully built 62a0abaa342a Successfully tagged aspnetapp:latest
This image has been successfully built and tagged. If desired, this image could be re-tagged using "docker tag" and pushed to any docker registry issuing a "docker push"command. We’ll skip that step for demonstration purposes and instead just run the container directly on our workstation using "docker run":
PS C:\Users\Administrator\Desktop\aspnet-containerized>docker run --name aspnet_sample --rm -it -p 443:443 -p 8000:80 aspnetapp Service 'w3svc' has been stopped Service 'w3svc' started
Note that the "docker run" command issued above uses some parameters:
- -name is used to give the running container a specific name, to distinguish it from other containers that might be running on the host.
- -rm tells the docker daemon to discard the container after is it stopped. If this parameter is not used, the state of the container would be preserved indefinitely and could be restarted if desired. It’s best to clean up using -rm when testing.
- -it tells docker to run in interactive mode and attach a tty. This displays any output directly to the console. To run the container in the background, use -d instead to run in "detached" mode.
- -p allows host:container port mappings. In this example, both HTTP and HTTPS ports are mapped. The container exposes port 80 and 443 (IIS inside of the container has bindings for these) and 8000 and 443 on the host are mapped, respectively.
- aspnetapp is the name of the image we are running, in this case, it assumes the aspnetapp:latest image tag that was built in the previous step.
Navigating to either the 443 port for HTTPS or the port 8000 on HTTP in a web browser shows the application responding. The sample application welcome page is displayed:
It’s simple to run existing ASP.NET applications in containers to take advantage of all of the benefits of Docker, giving you the choice of where to run your applications. It’s great to define your application images as code in Dockerfiles to ensure repeatability and portability. The base images that Microsoft publishes on Docker Hub provide a clean slate with IIS and additional configuration of the run time environment can be done via Powershell within the Dockerfile.
If you have any thoughts or questions please let us know in the comments below and feel free to reach out directly on GitHub!