Running Kentico in a Docker container

   —   

Containers are the next big thing in the world of deployment and DevOps. Two weeks ago, at NDC Oslo 2016, I attended Ben Hall's presentation on Deploying Docker Containers on Windows Server 2016. That triggered me to do what I had wanted to do for a long time — deploy Kentico to a Windows Server Container using Docker. This article will walk you through the process step by step.

Why Docker?

Docker Logo

Did you ever decide not to keep any of the DTAP environments on dedicated (virtual) machines just because it would be too expensive or too much work to maintain them? It doesn't always have to be a trade-off between the required level of isolation and maintenance/resource costs. Deploying an application to a Docker container is like deploying it to a VM, but without the bad bits. You get a high degree of isolation and portability while staying performant. Besides that, you get the ability to roll back to any previous version of the container, which is useful, especially if something goes wrong in production. Talking of production, scaling your app horizontally with Docker is a piece of cake — it takes just a few seconds to boot up more containers, either in your own infrastructure or in the cloud.

This set of features can make using Docker beneficial in all stages of your project — going from development to production. Plus, the fact that the containers can be scripted by developers aligns nicely with the idea of DevOps.

How Is It Different from Virtual Machines?

The difference between Docker and VMs is best described by the following scheme:

VM vs Container

Instead of booting a Guest OS for every app (VM), the Docker engine provides each container with resource isolation and allocation while sharing the operating system. Containers are created from images that contain everything that application needs to run, and can be terminated and started again at any time. Images guarantee the consistency of the containers. We can create images by building a Dockerfile - a file with a list of instructions (commands) needed to assemble the image. Images can be inherited and can build on top of each other (we call it layering). The first layer is always a container OS image that is immutable. In the realm of Windows, there are two types of containers: Windows Server Core and Nano Server containers. In this article, I'll be talking about the first one - Windows Server Core containers.

If the terminology (hosts, images, container) is still confusing for you, have a look at container fundamentals on MSDN.

Prerequisites

Operating System

Though it's possible to run containers on earlier versions of Windows, I recommend using either Windows Server 2016 Technical Preview 5 or Windows 10 Insider Preview Build 14352. If you want to use your client system, you need to become a "Windows Insider".

For the purposes of this article, I'll be using Windows Server 2016 TP5 installed on an Azure Virtual Machine.

Windows Containers

Once you have installed the correct operating system, you have to install container support. The installation steps are a bit different for WS2016 and W10:

 

Windows Server 2016

Windows 10

Install Container Feature

Step 1

Step 1

Install Docker

Step 2

Step 2

Install Base Container Images

Step 3

An alternative option is to let PowerShell take care of everything for you. Just run the following commands in your PS console:


# Download script
wget -uri https://aka.ms/tp5/Install-ContainerHost -OutFile C:\Install-ContainerHost.ps1
# Run this command on Windows Server 2016
.\Install-ContainerHost.ps1
# Run this command on Windows 10
.\Install-ContainerHost.ps1 -IgnoreClient
 All possible parameters are listed in the documentation on GitHub.

Kentico Web Project & Database

Make sure you have a Kentico instance running and that the SQL server is accessible from the host OS. Pack your web project into c:\kentico9\kentico9.zip.

In highly automated scenarios such as Continuous Integration, this manual step can be avoided by calling:


PS c:\> Compress-Archive -Path C:\<source control path>\Kentico9\ -DestinationPath c:\kentico9\kentico9.zip -CompressionLevel Fastest

Building a Docker Image

Now, let's put all the pieces of the puzzle together and build a docker image by creating a Dockerfile.

In short our Dockerfile will:

  • base our image on Window Server Core 2016 with pre-installed IIS (you can find the image on Docker Hub or by running "docker search" in PowerShell, you can also see its Dockerfile)
  • install ASP.NET 4.5
  • copy the Kentico web project 
  • map a virtual directory to the web project

Create a file with the following contents and save it as C:\kentico9\Dockerfile on your host operating system. Normally, you would store the file in your source control system.

Dockerfile


# Use Windows Server Core with IIS as a base image
FROM microsoft/iis:latest
# Install ASP.NET 4.5
RUN powershell -executionpolicy bypass -command "Add-WindowsFeature Web-Asp-Net45"
# Create a new folder inside the container
RUN mkdir c:\payload
# Use the new folder as a work directory for docker commands
WORKDIR /payload
# Copy Kentico9.zip from host (c:\kentico9) to container (WORKDIR = c:\payload)
# Alternatively, you could use ADD command which supports URLs and unzipping .tar.gz archives
COPY Kentico9.zip Kentico9.zip
# Run a PowerShell command inside the container to extract the archive
RUN powershell -executionpolicy bypass -Command "expand-archive -Path 'c:\payload\Kentico9.zip' -DestinationPath 'c:\inetpub\wwwroot\'"
# Run appcmd.exe to add virtual directory to the Default Web Site in IIS
RUN %systemroot%/system32/inetsrv/appcmd.exe set vdir "Default Web Site/" -physicalPath:"c:\inetpub\wwwroot\Kentico9\CMS"
 When writing a Docker file, you should follow best practices to understand the build cache and to avoid common mistakes.

 

Run the following command to build the image and tag it "kentico9".


PS c:\> docker build -t kentico9 c:\kentico9

Let's verify that our image has been built successfully:


PS c:\> docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
kentico9            latest              68c367d7e41b        7 hours ago         10.28 GB
windowsservercore   10.0.14300.1000     5bc36a335344        4 weeks ago         9.354 GB
windowsservercore   latest              5bc36a335344        4 weeks ago         9.354 GB
microsoft/iis       latest              c26f4ceb81db        5 weeks ago         9.49 GB
microsoft/iis       windowsservercore   c26f4ceb81db        5 weeks ago         9.49 GB

"kentico9" is in there. So far, so good.

 The image cache is stored at C:\ProgramData\docker\windowsfilter

Running the Docker Image

Now the fun part starts. Finally, let's run the image we've built.


#       docker run [OPTIONS]                                IMAGE    [COMMAND] [ARG...]
PS c:\> docker run --rm --name kentico9_instance1 -it -p 80 kentico9 cmd
 Parameters 

--rm Automatically removes the container when the command (cmd) exits

--name kentico9_instance1 Assigns a name to the container (otherwise a random but very intelligent name will be generated)

-p 80 Exposes port 80

-it Allocates a pseudo-TTY connected to the container’s stdin; creating an interactive shell in the container. (Simply put, it keeps the container alive.)

When you run the command, you should see a command prompt (which is running inside the container). If you exit the prompt, the container will be removed. Don't do that yet.

Docker container started up...

Let's first verify that our machine is running. Launch another instance of PowerShell and run the following command. You should see an image named "kentico9" running.


PS c:\> docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                NAMES
bf634a16abc9        kentico9            "cmd"               2 hours ago         Up 2 hours          0.0.0.0:80->80/tcp   kentico9_instance1

Connecting to the Container

Kentico running in Docker container

When we started the container, Docker assigned it a random IP address. To find all details about the images and containers, we use the "inspect" command:


#       docker inspect <container|image>
PS c:\> docker inspect kentico9_instance1
# Will output a long JSON object
 
# Let's use Select-String to "grep" the result
PS c:\> docker inspect kentico9_instance1 | sls IPAddress
 
            "SecondaryIPAddresses": null,
            "IPAddress": "",
                    "IPAddress": "172.30.3.234",

On your docker host system, open the browser and connect to the IP address. E.g. when I go to http://172.30.3.234/admin, I'm presented with a familiar logon screen. If you, for some reason, need to assign the container a static IP, configure a custom host name or do other networking magic, check out the MSDN documentation (or the Docker version).

Exposing the App through the Container Host

Our container is now running and is accessible from the container host via the IP. Let's take one step further, and expose the app to the rest of the world. Kill the running container by typing "exit" into the command prompt, and run the following command:


# Set -p 80:80 (-p host:container)
PS c:\> docker run --rm --name kentico9_instance1 -it -p 80:80 kentico9 cmd

If you get the "HNS failed with error : Failed to create endpoint." error, run the following command, restart the machine, and try again:


PS c:\> Get-NetNatStaticMapping | ? ExternalPort -eq 80 | Remove-NetNatStaticMapping

Adjusting Firewall Settings

Now, we need to open the HTTP port on our host machine:


PS c:\> New-NetFirewallRule -DisplayName "TCP 80" -Protocol TCP -LocalPort 80 -Action Allow -Enabled True

If you're running on Azure, you'll also need to add an inbound security rule to your network security group.

Setting up network security rules in Azure

And we're done. We can access a Kentico instance running in a Docker container via a public IP:

Kentico running in Docker accessed from the Internet

Next Steps

Kernel Level Isolation

Did I mention that you can run a Docker container inside a Hyper-V virtual machine? It's as easy as specifying the --isolation parameter of the run command.


# Set --isolation=hyperv
PS c:\> docker run --isolation=hyperv --rm --name kentico9_instance1 -it -p 80:80 kentico9 cmd

Note that this is not possible in scenarios where the host operating is virtualized itself and the hardware virtualization is not being passed from hypervisor to the VM. This is the case of Azure VMs. However, if you have control over the infrastructure in your organization, you should be able to configure hardware (Intel VT-x, AMD-V or similar) virtualization passthrough to enable nested hypervisors. Both Hyper-V and vSphere support that.

Mounting Persistent Storage

Sometimes, you may need to create a folder that is shared across more containers or with the container host (e.g., for media files). Another time you need a folder that will survive container disposal. These scenarios can be accomplished using data volumes.

Here's how to mount a data volume:


# Set -v c:/<hostPath>:/c:/<containerPath>
PS c:\> docker run -v c:/Users/guest/:/c:/<containerPath> --rm --name kentico9_instance1 -it -p 80:80 kentico9 cmd

Conclusion

Docker has a great potential for deploying applications. I hope I have managed to demonstrate at least part of it. As you can see, all steps involved in the process can be scripted and, therefore, the whole process can be automated. This means we can plug it into CI and make our deployments a little more pleasant and consistent.

Let me know if you have found the article useful and if the examples worked for you. 

 Disclaimer:

No animals were harmed in the writing of this article. Just two pull requests were submitted.

Share this article on   LinkedIn

Petr Svihlik

I listen to the voice of community and act in its interest.