Anthony Chu Contact Me

ASP.NET Web.config Transforms in Windows Containers - Revisited

Wednesday, November 15, 2017

When I last blogged about ASP.NET 4.x applications and web.config transformations in Windows containers, I was baking the transform files in the container images themselves. While this worked, the secrets were stored inside the container images. A much better approach is to supply the transformation at the time of container startup; we can do this by mounting a file when starting the container.

Configuration: the "Docker way"

As we discussed in the post about overriding web.config settings using environment variables, one of the biggest benefits of using containers is the ability to wrap your application and its dependencies into an immutable image. This image is built once and deployed to different environments as it progresses through a continuous delivery pipeline. As the application is deployed, we can pass variables or mount configuration files into the container to configure it for the given environment. This ensures that we deploy the exact same application to production as the one tested in other environments.

This approach also helps keep secrets out of container images.

Extending the microsoft/aspnet base image to perform web.config transformations

The easiest way to containerize an ASP.NET application is using the microsoft/aspnet base image. It's as easy as a 3-line Dockerfile:

FROM microsoft/aspnet:4.7.1-windowsservercore-1709
WORKDIR /inetpub/wwwroot
COPY . .

Here, we're using the 4.7.1 Windows Server Core 1709 base image, but there are images for other versions as well and they all work the same way.

We need to create a modified version of the microsoft/aspnet image that allows us to apply web.config transformations. The technique to do this is fairly simple:

  1. Add a command-line utility (WebConfigTransformRunner) to the container image for running web.config transformations
  2. Create a script (Set-WebConfigSettings.ps1, see below) that checks if a web.config transform file is available at a predefined location and run the transformation
  3. Override the entry point of the microsoft/aspnet base image to call that script at container startup

Install WebConfigTransformRunner

Here's the Dockerfile to create our custom version of microsoft/aspnet that includes the ability to apply configuration transformations:

FROM microsoft/aspnet:4.7.1-windowsservercore-1709

WORKDIR /
RUN $ProgressPreference = 'SilentlyContinue'; \
    Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile \Windows\nuget.exe; \
    $ProgressPreference = 'Continue'; \
    \Windows\nuget.exe install WebConfigTransformRunner -Version 1.0.0.1

RUN md c:\aspnet-startup
COPY . c:/aspnet-startup
ENTRYPOINT ["powershell.exe", "c:\\aspnet-startup\\Startup.ps1"]

We use NuGet to bring in the WebConfigTransformRunner utility. We also call our custom startup script (below) as our entry point.

Startup.ps1

This is the script that runs at container startup. The second command is the entry point from the original Docker image that monitors the w3svc service.

C:\aspnet-startup\Set-WebConfigSettings.ps1 -webConfig c:\inetpub\wwwroot\Web.config
C:\ServiceMonitor.exe w3svc

Set-WebConfigSettings.ps1

This is the script that does the hard work. We simply check if a file exists at c:\web-config-transform\transform.config, and use it to transform the web.config:

param (
    [string]$webConfig = "c:\inetpub\wwwroot\Web.config"
)

## Apply web.config transform if exists

$transformFile = "c:\web-config-transform\transform.config";

if (Test-Path $transformFile) {
    Write-Host "Running web.config transform..."
    \WebConfigTransformRunner.1.0.0.1\Tools\WebConfigTransformRunner.exe $webConfig $transformFile $webConfig
    Write-Host "Done!"
}

Image in Docker Hub

If you want to try this out, I've published the image to Docker Hub at anthonychu/aspnet (the 1709 image requires Windows 10 Fall Creators update or Windows Server version 1709). This image also includes the ability to override settings with environment variables, as described in this article.

Using the new image

To test this out, we'll create an ASP.NET WebForms project and add an app settings and a connection string to Web.config:

<configuration>
  <appSettings>
    <add key="PageTitle" value="Hello World" />
  </appSettings>
  <connectionStrings>
    <add name="DefaultConnection" connectionString="Data Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\Foo.mdf;Integrated Security=True" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- ... -->
</configuration>

To containerize the application, first we need to build it. Then we build the container image using this Dockerfile. It's the same Dockerfile as we would normally use, except it uses the base image we created earlier:

FROM anthonychu/aspnet:4.7.1-windowsservercore-1709
WORKDIR /inetpub/wwwroot
COPY . .

Start a container normally and see the original Web.config values displayed:

PS> docker run -d -p 80:80 sample-aspnet-4x

Now, we create the following web.config transform file in a folder on our local machine (C:\transform\transform.config). The transform changes the value of the DefaultConnection connection string.

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <connectionStrings>
      <add name="DefaultConnection" 
        connectionString="Data Source=DevSQLServer;Initial Catalog=MyDevDB;Integrated Security=True" 
        xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
    </connectionStrings>
</configuration>

Start another container, this time we'll mount the configuration transformation file we created to a directory at C:\web-config-transform inside the container:

PS> docker run -d -p 80:80 `
    -v C:\transform:C:\web-config-transform `
    sample-aspnet-4x

We should see the transformed settings being used:

Source code

https://github.com/anthonychu/aspnet-env-docker

Also check out my article on how to override web.config settings using environment variables.