David Shifflet's Snippets

Mindset + Skillset + Toolkit = Success




< Back to Index

Creating Docker Containers for Legacy Windows Console Applications


This is a continuation of a post about Legacy Windows Applications

In this example we are going to create an image for starting a container in Docker. This container will provide a http post endpoint to take a file and provide back some converted result from a "legacy" application. If you read this post it will show you how to automate the legacy application installs and that provides a good start for containerizing a legacy windows application.

The use case for this example is wanting to run a legacy windows console application and post files to it to be ran and then receive the output. If all you want to run is a .net core WebApi or MVC application there are easier ways to do that.

In this example we are going to use:

The information contained in this example can be paired with this blog post to dockerize legacy windows applications.

Creating the Docker Build

Create an empty directory and call it what you are going to name the image. I used "http-zip-service". This will be our "build" directory.

In this folder place the contents of the ConsoleCommandProxy publish. Create an empty text file called "dockerbuild". We need the ConsoleCommandProxy project because we need a way to send files to the legacy console application and get the results from outside of the container.

In this example the Compress-Archive cmdlet in Powershell will simulate our legacy application.

When we run "docker build" in a directory the "dockerbuild" file controls how docker creates the image. Let's put some content in the docker build file.


# Our base image is windows server core.  You can browse images at hub.docker.com
FROM mcr.microsoft.com/windows/servercore:ltsc2019

# We want to use the power shell as we build it
SHELL ["powershell","-command"]

# Create directory for our application
RUN New-Item -Path $env:systemdrive\app -Type Directory

# Copy over the contents from the current directory to the directory we created on the container.  This is a copy from host to container.  The directory would contain the console application and the ConsoleCommandProxy project publish output.
COPY . /app

# Make the working directory our application's directory
WORKDIR /app
dockerbuild - Comments are lines that start with #.

Fairly simple and straight forward. Next we need to change things in the HttpHost files.

Change the file "hosting.json" to:

{
    "server.urls": "http://0.0.0.0:5000"
}
hosting.json
This will allow the ConsoleCommandProxy to use any IP address in the container and communicate via port 5000.

Next change the file "appsettings.json" to:

{
    "Logging": {
    "LogLevel": {
        "Default": "Warning"
    }
    },
    "AllowedHosts": "*",
    "OutputExtension" : ".zip",
    "Command" : "powershell",
    "Arguments" : "Compress-Archive -Path \"{0}\" -CompressionLevel Optimal -DestinationPath \"{1}\"",
    "TimeoutInMs" : 5000
}
appsettings.json

This will use the Compress-Archive cmdlet in Powershell to create a zip file. A user will post a file and then receive a compressed file back. The powershell cmdlet "CompressArchive" is our legacy application for this example.

The above settings for the ConsoleCommandProxy define that when a file is posted (input file is {0} in the arguments), it will run a command line exe and return the output file (output is {1} in the arguments). The output extension is ".zip" because we want that extension all the time. You can leave this as "" if you just want to use the output extension. And the command is "powershell.exe".

When a user posts a file to http://some-ip:5000 it will take the file run the command using the arguments and send back the zipped output of the command to the user.

Now let's build a container and see how it works.

From the command line go to the directory where the build files are and run the command:

docker build -t http-zip-service .

The following should happen:

Sending build context to Docker daemon  252.4kB
Step 1/5 : FROM mcr.microsoft.com/windows/servercore:ltsc2019
    ---> 3e9dc86c64a9
Step 2/5 : SHELL ["powershell","-command"]
    ---> Using cache
    ---> b68ed2666aea
Step 3/5 : RUN New-Item -Path $env:systemdrive\app -Type Directory
    ---> Using cache
    ---> 14f8db39bc2f
Step 4/5 : COPY . /app
    ---> b270cc0831d2
Step 5/5 : WORKDIR /app
    ---> Running in 2c1d8ccf5954
Removing intermediate container 2c1d8ccf5954
    ---> 1f92f8f36938
Successfully built 1f92f8f36938
Successfully tagged http-zip-service:latest
Console Output

This will have docker build an image from the current directory named http-zip-service and not run it.

Next we will start the image in a container and poke around and see if we can make the HttpHost run. This is where we would make sure that the legacy application is working right inside of the container. If it doesn't work we can troubleshoot and change the continer build and install.ps1 scripts at this time to get it working.

In the command line type:

docker run -it http-zip-service powershell

This will have docker start a container using the "http-zip-service" image in interactive and terminal mode and start Powershell for us. This effectively starts the container and logs us into the container's console.

You will see this:

Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

PS C:\app>
Console Output

Now let's start the app. Run the following in the shell on the container:

PS C:\app> dotnet httphost.dll
dotnet : The term 'dotnet' is not recognized as the name of a cmdlet,
function, script file, or operable program. Check the spelling of the name,
or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ dotnet httphost.dll
+ ~~~~~~
    + CategoryInfo          : ObjectNotFound: (dotnet:String) [], CommandNotF
    oundException
    + FullyQualifiedErrorId : CommandNotFoundException

Console Output

Unfortunately the HttpHost is a .NET CORE 2.2 application and the .NET CORE 2.2 runtimes are not installed in the container. Let's fix that.

Exit the container. It will stop.

In the build directory on the host create a "install.ps1" file and use the following text:

$ErrorActionPreference = 'Stop'

$system32path = [System.Environment]::SystemDirectory
$net22File = "$pwd\dotnet-hosting-2.2.6-win.exe"

Write-Host "Downloading NET Core 2.2 Runtime & Hosting Bundle for Windows (v2.2.6)"
(New-Object System.Net.WebClient).DownloadFile("https://download.visualstudio.microsoft.com/download/pr/a9bb6d52-5f3f-4f95-90c2-084c499e4e33/eba3019b555bb9327079a0b1142cc5b2/dotnet-hosting-2.2.6-win.exe", $net22File)

Write-Host "Installing NET Core 2.2 Runtime & Hosting Bundle for Windows (v2.2.6)"
Start-Process $net22File -ArgumentList "/quiet" -Wait

Remove-Item -path $net22File
install.ps1

Now edit your "dockerfile" to look like:

FROM mcr.microsoft.com/windows/servercore:ltsc2019
SHELL ["powershell","-command"]

RUN New-Item -Path $env:systemdrive\app -Type Directory
COPY . /app
WORKDIR /app

# Run the install powershell script
RUN .\install.ps1
dockerfile

Now let's rebuild the image:

docker build -t http-zip-service .

It should build.

Sending build context to Docker daemon    254kB
Step 1/6 : FROM mcr.microsoft.com/windows/servercore:ltsc2019
    ---> 3e9dc86c64a9
Step 2/6 : SHELL ["powershell","-command"]
    ---> Using cache
    ---> b68ed2666aea
Step 3/6 : RUN New-Item -Path $env:systemdrive\app -Type Directory
    ---> Using cache
    ---> 14f8db39bc2f
Step 4/6 : COPY . /app
    ---> 69d57139ab61
Step 5/6 : WORKDIR /app
    ---> Running in 5680b64e2bcc
Removing intermediate container 5680b64e2bcc
    ---> a124d026ac73
Step 6/6 : RUN .\install.ps1
    ---> Running in fb18516e1785
Downloading NET Core 2.2 Runtime & Hosting Bundle for Windows (v2.2.6)
Installing NET Core 2.2 Runtime & Hosting Bundle for Windows (v2.2.6)
Removing intermediate container fb18516e1785
    ---> 8e8e7fcaf3cb
Successfully built 8e8e7fcaf3cb
Successfully tagged http-zip-service:latest

Console Output

Now start it and try to run the app again.

docker run -it http-zip-service powershell

It should work when you run "dotnet httphost" from the command line.

PS C:\app> dotnet httphost.dll
Hosting environment: Production
Content root path: C:\app
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
Console Output

Now it is working. Press CTRL+C on the keyboard and exit the app then quit from the console which will exit the container.

Change the "dockerfile" to include an entry point at the bottom:

FROM mcr.microsoft.com/windows/servercore:ltsc2019
SHELL ["powershell","-command"]

RUN New-Item -Path $env:systemdrive\app -Type Directory
COPY . /app
WORKDIR /app

# Run the install powershell script
RUN .\install.ps1

# Once the container starts run this...
ENTRYPOINT ["dotnet", "httphost.dll"]
dockerfile

Rebuild the image:

docker build -t http-zip-service .

Now we are going to run the image in "detached" mode with ports published. Detached mode means the containers will run in the background. We can view what is running by using the command "docker ps". The port 5000 in the container will be published to the host on port 5001. This way we can open a browser (outside of the container) and connect to the service inside the container.

The command we want to run is:

docker run -d -p 5001:5000 http-zip-service

More Information

If you want to see running containers:

docker ps

If you want to see all containers:

docker ps -a

If you want to stop a container:

docker stop <container-id>

If you want to start a stopped container:

docker start <container-id>

If you want to remove a container:

docker rm <container-id>

Docker will capture the stdout from a container and log it. To view the logs for a container use:

docker logs <container-id>

If you open a browser in the host and go to the url http://localhost:5001/health you should see "Healthy". Or from the command line:

curl http://localhost:5001/health

Let's see what happens when we try to zip a file via http. From the command line run:

curl -v -F file=@appsettings.json http://localhost:5001 --output "test.zip"

A test.zip should be created and will contain the original file.

When Things Go Wrong:

  • Typos. Lots of text files and typing means lots of chances for typos. Double check the syntax and variable names in scripts.
  • If your docker build fails and you want to see what state the container was in at that build step read this stackoverflow.com.
  • If you want to attach a terminal to a running container use the docker attach or docker exec commands.
  • Viewing the event viewer in windows server core. There is no GUI in Windows Server Core. If something goes wrong with your install in the container you may need to access the information contained in the Event Viewer.

In the next blog post let's move away from the numeric IP address and do service discovery with Docker, Consul and Registrator.