Use Docker To Breathe New Life for Your ASP.NET Apps
Using Docker's lightweight containerization technology, you can modernize and extend ASP.NET WebForms app quickly and safely on the Microsoft Azure cloud. Here's how.
- By Elton Stoneman
Developers have seen quite a bit of success using Microsoft's .NET Framework for well over 15 years, with countless business-critical apps running on older versions of the Framework and older versions of Windows Server. Many of those apps still offer great business value, but they're likely to be difficult to maintain, upgrade, extend and manage, and they may not justify the investment needed for a full rewrite.
Mixing Docker's platform for running applications in lightweight containers and Windows Server 2016, you can give traditional apps a new lease on life without a lengthy and expensive rebuild project.
In this two-part article I'll first take a monolithic ASP.NET WebForms app that connects to a SQL Server database, and I'll modernize it by taking advantage of the Docker platform. I'll start by moving the whole app as is to Docker, without any code changes, and run the Web site and database in lightweight containers. In part 2, I'll show a feature-driven approach to extending the app, improving performance and giving users self-service analytics. With the Docker platform you'll see how to iterate with new versions of the app, upgrade the components quickly and safely, and deploy the complete solution to Microsoft Azure.
Where Docker Fits in .NET Solutions
Docker is for server applications--Web sites, APIs, messaging solutions and other components that run in the background. You can't run desktop apps in Docker because there's no UI integration between the Docker platform and the Windows host. That rules out running Windows Forms or Windows Presentation Foundation (WPF) apps in containers (although you could use Docker to package and distribute those desktop apps), but Windows Communication Foundation (WCF), .NET console apps and all flavors of ASP.NET are great candidates.
To package an application to run in Docker, you write a small script called a Dockerfile that automates all the steps for deploying the app. Typically this includes Windows PowerShell commands for configuration and instructions to copy application content and set up any dependencies. You can unzip compressed archives or install MSIs, too, but the packaging process is all automated, so you can't run an install process that has a Windows UI and needs user input.
When you're looking at a solution architecture to figure out which parts can run in Docker containers, keep in mind any component that can be installed and run without the Windows UI is a good candidate. This article focuses on .NET Framework apps, but you can run anything in a Windows container that runs on Windows Server, including .NET Core, Java, Node.js and Go apps.
Migrating .NET Apps to Containers
How you migrate to Docker depends on how you're currently running your app. If you have a fully configured app running in a Hyper-V VM, the open source Image2Docker tool can automatically generate a Dockerfile from the VM's disk. If you have a build process that publishes an MSI or a WebDeploy package, it's easy to write your own Dockerfile by using one of Microsoft's base images on Docker Hub.
Here's a complete Dockerfile that scripts the packaging of an ASP.NET WebForms app into a Docker image:
RUN Remove-Website -Name 'Default Web Site'; \
New-Item -Path 'C:\web-app' -Type Directory; \
New-Website -Name 'web-app' -PhysicalPath 'C:\web-app' -Port 80 -Force
RUN Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters' \
-Name ServerPriorityTimeLimit -Value 0 -Type DWord
COPY ProductLaunch.Web /web-app
Nine lines of script are all I need, and there are no application changes. This could be an ASP.NET 2.0 app, currently running on Windows Server 2003--with this Dockerfile I can build it into an image that immediately upgrades the app to Windows Server 2016 and the .NET Framework 4.5. I'll walk through each of those instructions:
- FROM microsoft/aspnet tells Docker which image to use as the starting point. In this case, it's a Microsoft image with IIS and ASP.NET installed on top of a specific version of Windows Server Core.
- SHELL ["powershell"] changes to a different shell for the rest of the Dockerfile, so I can run PowerShell cmdlets.
- RUN Remove-Website uses PowerShell to set up IIS, removing the default Web site and creating a new one with a known location for the application.
- EXPOSE 80 opens port 80 explicitly to allow network traffic into the container as Docker containers are locked down by default.
- RUN Set-ItemProperty turns off the Windows DNS cache inside the image, so any DNS requests get served by Docker.
- COPY ProductLaunch.Web copies the published Web site project from the ProductLaunch.Web directory on the host into the image.
The Dockerfile is like a deployment guide for the Web application, but instead of being a vague human document, it's a precise and actionable script. To produce the packaged app I run the docker build command from the directory that contains the Dockerfile and the published Web site:
docker build --tag sixeyed/msdn-web-app:v1 .
This command builds a Docker image with the name sixeyed/msdn-web-app and the tag v1. The name contains my user account for the Hub (sixeyed), so I can share this image by signing in with my credentials and pushing it to the Hub. Tags are useful for versioning images, so when I package a new version of the application, the image name will stay the same, but the tag will be v2.
Now I can run a container from the image and that will start the application, but the sample app has a dependency on SQL Server so I need SQL Server running before I can start the Web site.
Pulling Dependencies from Docker Hub
Docker has a networking stack that lets containers reach each other over a virtual network, and also lets containers reach external hosts running on the physical network. If I had a SQL Server instance running on a machine in the network, the ASP.NET app in the container could use it—I'd just need to specify the server name in the connection string. Or I can run SQL Server in a container, and the Web app will be able to reach it by using the container name in the connection string.
SQL Server Express is available on Docker Hub in an image maintained by Microsoft. To start a database container from that image, I run:
docker run --detach `
--publish 1433:1433 `
--env sa_password=MSDNm4g4z!n3 `
--env ACCEPT_EULA=Y `
--name sql-server `
This starts a container in the background with the detach flag and publishes port 1433, so I can connect to the SQL instance in the container from outside, perhaps using SQL Server Management Studio on the host. The env options are key-value pairs, which Docker surfaces inside the container as system environment variables. The SQL Server image uses these values to confirm that the license agreement has been accepted, and to set the password for the sa user.
To run a container, Docker needs to have a copy of the image locally. Distribution is built into the Docker platform, so if you don't have the SQL Server Express image locally when you run this command, Docker will download it from the Hub. There are more than half a million images on Docker Hub, which have been downloaded more than 9 billion times. Docker started in the Linux world and the majority of those images are Linux apps, but there are a growing number of high-quality Windows apps you can download and drop straight into your solution.
SQL Server is running in a Docker container now, and my Web app uses sql-server as the hostname in the connection string so it will connect to the database running in Docker. I can start the WebForms application in the background and publish port 80 to make the Web site accessible:
docker run --detach `
--publish 80:80 `
If an external machine sends a request on port 80 to my host, Docker receives the request and transparently forwards it to the ASP.NET app running in the container. If I'm working on the host, I need to use "docker inspect" to get the container's IP address and browse to the container to see the site, which is a simple product launch microsite. You can see the data capture page from the site running in Docker in Figure 1.
Run "docker ps" and you'll see a list of all running containers. One is a database and one is a Web application, but you manage them both in the same way: "docker top" shows you the processes running in the container; "docker logs" shows you the log output from the app; and "docker inspect" shows you which ports are open and a host of other information about the container. Consistency is a major benefit of the Docker platform. Apps are packaged, distributed and managed in the same way, no matter what technology they use.
Next week, we'll move the app wholesale to Docker, but we'll take a targeted approach. Stay tuned.
Note: Thanks to Mark Heath, who reviewed this article.
Mark Heath is a .NET developer specializing in Azure, creator of NAudio, and an author for Pluralsight. He blogs at markheath.net and you can follow him on Twitter @mark_heath.
Elton Stoneman is a seven-time Microsoft MVP and a Pluralsight author who works as a developer advocate at Docker. He has been architecting and delivering successful solutions with Microsoft technologies since 2000, most recently API and Big Data projects in Azure, and distributed applications with Docker.