Writing a Dockerfile for NerdDinner
I'll follow the multi-stage build approach for NerdDinner, so the Dockerfile for the dockeronwindows/ch-02-nerd-dinner images starts with a builder stage:
# escape=`
FROM sixeyed/msbuild:netfx-4.5.2-webdeploy-10.0.14393.1198 AS builder
WORKDIR C:\src\NerdDinner
COPY src\NerdDinner\packages.config .
RUN nuget restore packages.config -PackagesDirectory ..\packages
COPY src C:\src
RUN msbuild .\NerdDinner\NerdDinner.csproj /p:OutputPath=c:\out\NerdDinner `
/p:DeployOnBuild=true `
/p:VSToolsPath=C:\MSBuild.Microsoft.VisualStudio.Web.targets.14.0.0.3\tools\VSToolsPath
The stage uses sixeyed/msbuild as the base image for compiling the application, which is an image I maintain on Docker Cloud. That image installs MSBuild, NuGet and the other dependencies you need for packaging a Visual Studio Web project, without using Visual Studio. The build stage happens in two parts:
- First, copy the NuGet packages.config file into the image, and then run nuget restore
- Next, copy the rest of the source tree and run msbuild
Separating those parts means Docker will use multiple image layers, the first layer will contain all the restored NuGet packages and the second layer will contain the compiled web app. This means I can take advantage of Docker's layer caching. Unless I change my NuGet references, the packages will be loaded from the cached layer and Docker won't run the restore part, which is an expensive operation. The MSBuild step will run every time any source files change.
If I had a deployment guide for NerdDinner, before the move to Docker, it would look something like this:
- Install Windows on a clean server
- Run all Windows Updates
- Install IIS
- Install .NET
- Set up ASP.NET
- Copy the web app into the C drive
- Create an application pool in IIS
- Create the website in IIS using the application pool
- Delete the default website
This will be the basis for the second stage of the Dockerfile, but I will be able to simplify all the steps. I can use microsoft/aspnet as the FROM image, which gives me a clean install of Windows with IIS and ASP.NET installed. That takes care of the first five steps in one instruction. This is the remainder of the Dockerfile for dockeronwindows/ch-02-nerd-dinner:
FROM microsoft/aspnet:windowsservercore-10.0.14393.1198
SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop';"]
WORKDIR C:\nerd-dinner
RUN Remove-Website -Name 'Default Web Site'; `
New-Website -Name 'nerd-dinner' -Port 80 -PhysicalPath 'c:\nerd-dinner' -ApplicationPool '.NET v4.5'
RUN & c:\windows\system32\inetsrv\appcmd.exe unlock config /section:system.webServer/handlers
COPY --from=builder C:\out\NerdDinner\_PublishedWebsites\NerdDinner C:\nerd-dinner
Using the escape directive and SHELL instruction lets me use normal Windows file paths without double backslashes and PowerShell-style backticks to separate commands over many lines. Removing the default website and creating a new website in IIS is simple with PowerShell, and the Dockerfile clearly shows me the port the app is using and the path of the content.
I'm using the built-in .NET 4.5 application pool, which is a simplification from the original deployment process. In IIS on a VM, you'd normally have a dedicated application pool for each website in order to isolate processes from each other. But in the containerized app, there will be only one website running - another website would be in another container, so we already have isolation, and each container can use the default application pool without worrying about interference.
The final COPY instruction copies the published web application from the builder stage into the application image. It's the last line in the Dockerfile to take advantage of Docker's caching again. When I'm working on the app, the source code is the most frequent thing to change. The Dockerfile is structured so that when I change code and run docker image build the only instructions that run are MSBuild in the first stage and the copy in the second stage, so the build is very fast.
This could be all you need for a fully functioning Dockerized ASP.NET website, but in the case of NerdDinner, there is one more instruction, which proves that you can cope with awkward, unexpected details when you containerize your application. The NerdDinner app has some custom configuration settings in the system.webServer section of its Web.config file, and by default that section is locked by IIS. I need to unlock the section, which I do with appcmd in the second RUN instruction.
Now I can build the image and can run a legacy ASP.NET app in a Windows container:
docker container run -d -P dockeronwindows/ch02-nerd-dinner
I can get the container's IP address with docker container inspect, and browse to the NerdDinner homepage:
At this point, the app isn't fully functional - I just have a basic version running. The Bing Maps object doesn't show a real map because I haven't provided an API key. The API key is something that will change for every environment (each developer, the test environments, and production will have different keys). In Docker you manage environment configuration with environment variables, which I will use for the next iteration of the Dockerfile in Chapter 3, Developing Dockerized .NET and .NET Core Applications.
If you navigate around this version of NerdDinner and try to register a new user or search for a dinner, you'll see a yellow screen crash page telling you the database isn't available. In its original form, NerdDinner uses SQL Server LocalDB as a lightweight database and stores the database file in the app directory. I could install the LocalDB runtime into the container image, but that doesn't fit with the Docker philosophy of having one function per container. Instead, I'll build a separate image for the database so I can run it in its own container.
I'll be iterating on the NerdDinner example in the next chapter, adding environment variables, running SQL Server as a separate component in its own container, and demonstrating how you can start modernizing traditional ASP.NET apps by making use of the Docker platform.