Build a Web Server Cluster using Docker, Linux and Windows in 1 Hour


The Docker platform has moved forward fast since Docker and Microsoft announced late last year that it would run on the Windows platform, particularly Windows 2016 Server. According to a recent Infoworld article, Docker now runs Linux and Windows in the same cluster. Additionally, the release of the 17.06 Docker Engine has moved us closer to the multi-platform Docker swarm reality than ever before.

So how does this work in practice? This article walks you through the process of building a Docker Swarm based web service that contains both Linux and Windows server containers. This can be done in about an hour.

The Basics

When containers stormed onto the scene a few years ago, to be honest, I was not impressed. I recall thinking: “We’ve had Linux namespaces, network isolation, file system isolation, etc. for years — big deal!” However, the genius of the container world, with Docker being first-of-mind in this space, is that a nice wrapper placed around these technologies makes them accessible as a group, programmable, and scriptable. Now, creating isolated processes is a simple call to an API — no master Linux/UNIX skills needed. Fast forward five years and look what Docker and containers have done to the industry. Wow!

Containers at the rudimentary level can be thought of as “encapsulated processes or applications.” Everything needed for an application or service to run is packaged into one running process on a server. The deployment speed of the container is literally as fast as starting a new process (if the image used to launch the container is already cached). You can achieve remarkable container density as compared to their older cousins, the virtual machine. For more on what containers are and the images used to build them, see

With roots in the Linux world, Linux containers have dominated the scene, and still do today. However, Microsoft is now playing along. It is clear the hatchet has been buried because you can now run the same application on both platforms at the same time in a Docker Swarm! For those of us who have been around the block, we never thought this day would come.

Curious? Here is a quick tutorial on how to run an Nginix and IIS cluster in a Docker Swarm using both Microsoft and Linux based containers.


For this walk-through, you’ll need access to at least one Linux server or VM that supports Docker (I used Ubuntu 16.04) and one Windows 2016 server or VM running on the same network. I used Google Cloud Platform to host mine, but you can choose whatever you like, even VMs on your local system.

My original, never-used-before hostnames for this walkthrough were windows-1 and ubuntu-1.

In addition, you must use the EE version of the Docker engine on Windows, so I opted to use it on both platforms to keep things aligned as closely as possible. This means you’ll need a license, which you can get for free (for a 30-day trial) at

After you have signed up, there is a link for setup instructions, and when you click on the link you will find a URL that is unique to your trial. Capture it, as you’ll need it later:

Install Docker

To start, install the Docker engine on each system to be used. Since I am not a fan of opening multiple browser tabs to get a task done, I’ll summarize the steps here, but you can refer to this link for Ubuntu Linux (other derivatives are in the navigation menu on the left) and this one for Windows for more information.

Ubuntu 16.04

  1. Remove older versions of Docker, if necessary:

$ sudo apt-get remove docker docker-engine docker-ce

2. Prepare the prerequisite packages and grab the Docker GPG key:

$ sudo apt-get update
$ sudo apt-get install apt-transport-https ca-certificates curl Software-properties-common
$ curl -fsSL <DOCKER-EE-URL>/ubuntu/gpg | sudo apt-key add -

The key should be DD91 1E99 5A64 A202 E859 07D6 BC14 F10B 6D08 5F96, confirmed by running:

$ apt-key fingerprint 6D085F96

3. Add the repository, replacing <DOCKER-EE-URL> with the URL you grabbed from your Docker trial above:

$ sudo add-apt-repository 
“deb [arch=amd64] <DOCKER-EE-URL>/ubuntu 
$(lsb_release -cs) 

4. Finally, install Docker:

$ sudo apt-get update
$ sudo apt-get install docker-ee

5. Test Docker — right now, Docker is only available to the root user, and you can add your ID to the docker group to gain access. For this guide, we’ll simply run as root:

$ sudo su -
# docker container run hello-world
Unable to find image ‘hello-world:latest’ locally 
latest: Pulling from library/hello-world
b04784fba78d: Pull complete
Digest: sha256:f3b3b28a45160805bb16542c9531888519430e9e6d6ffc09d7…
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.

Windows Server 2016

1. Open an elevated (Administrator) PowerShell command prompt, and run the commands:

PS> Install-Module -Name DockerMsftProvider -Force
PS> Unregister-Package Source -Provider Name DockerMsftProvider -Name Docker Default -Error action Ignore
PS> Register-Package Source -ProviderName DockerMsftProvider -Name Docker -Location
PS> Install-Package -Name docker -ProviderName DockerMsftProvider -Source Docker -Force
PS> Restart-Computer -Force

2. After the reboot, confirm the Docker service is running; if not start it manually.

3. Open an elevated (Administrator) PowerShell command prompt again and test your installation:

PS> docker container run hello-world:nanoserver
Unable to find image ‘hello-world:nanoserver’ locally
nanoserver: Pulling from library/hello-world
bce2fbc256ea: Pull complete
3ac17e2e6106: Pull complete
8cac44e17f16: Pull complete
5e160e4d8db3: Pull complete
Digest: sha256:25eac12ba40f7591969085ab3fb9772e8a4307553c14ea72d0e…
Status: Downloaded newer image for hello-world:nanoserver
Hello from Docker!
This message shows that your installation appears to be working correctly.

4. To save time later, go ahead and download (pull) the image we will use later onto the Windows server. It seems that the Microsoft images are much larger than most Linux images:

PS> docker pull microsoft/iis

Create the Swarm

You can create a swarm from either platform, IF you correctly copy the link presented from one platform to the other, as shown below. In my testing, I discovered that if I created the swarm on a Linux server, the Windows docker could not join successfully, as the wrap in the terminal of the command was carrying over as a carriage return in the PowerShell prompt. If I removed any line break, it worked. Be sure you get the full command on a single line, or you will be misled as I was in my testing.

Windows Server 2016

Open another elevated prompt again as before (in all probability, your first one is still pulling that image), and issue the command to create the swarm. You’ll receive output that gives you a link to use for other nodes to join. Copy this command to use on the Linux server. The advertise-addr is needed if your system is multi-homed — it tells the Docker swarm on which IP to be listening. Since I was on Google Cloud, I used the internal IP interface to keep things simple:

PS> docker swarm init — advertise-addr
Swarm initialed: current node (xyz) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join — token SWMTKN-1–4xdjyknpemyepex3pydc5cduoxnlpy5jdgz0hy1ovr6dtnmf2u-37x1pwimpo079uixsdtpn2o1d


Join the swarm by running the docker swarm join command that was presented above on the Linux server. If you lost that output, you can run docker swarm join-token worker on the Windows node to see the command again.

Windows Server 2016

One last time you will return to the Windows server. Since I am more comfortable in the Linux world, and there are tools there that are not available by default on Windows (grep, for example), I promoted my Linux node to a manager. A manager node can run all swarm-related commands; worker nodes cannot.

1. List the nodes (again, note the original hostnames of my VMs). The Medium Blog doesn’t quite format the output well:

PS> docker node ls
ajat… windows-1 Ready Active Leader
qrcp… ubuntu-1 Ready Active Reachable

2. Promote the Linux node to a manager:

PS> docker node promote ubuntu-1

Now, we can do all we need to do from our Linux server, which will be the assumption moving forward.

Label the Nodes

Labeling nodes in a swarm is a key feature in the Docker engine. These labels allow you to designate affinity when deploying a service or container, putting the containers for a given service on a particular subset of nodes in the swarm. For example, you could have nodes with faster storage attached; labeling the nodes with an indicator of this enables you to direct I/O-intensive containers to those nodes if capacity exists there.

For our purposes, we need to direct Windows or nano-based containers to the Windows node and Linux containers to the Linux node:

# docker node update — label-add windows windows-1
# docker node update — label-add linux ubuntu-1

Deploy two services into the swarm

Hopefully during all this extra work, the docker pull command has been merrily cranking along, grabbing the IIS image for our use. The second service will not deploy until that image is available, so you have to be patient and let it finish.

1. Launch a Linux-based service, mapping port 8000 to 80, and affiliating the service to run on the Linux node:

# docker service create — name nginx-linux — replicas=2 
 — publish 8000:80 — placement-pref spread=node.labels.linux nginx

2. Examine the service and note that the 2 containers are on the same node (which is not the default behavior) because of our label usage:

# docker service ls
ID NAME MODE REPLICAS IMAGE PORTS ullugr7… nginx-linux replicated 2/2 nginx:latest *:8000->80/tcp
# docker service ps nginx-linux
sheq4… nginx-linux.1 nginx:latest ubuntu-1 Running Running 2 minu
S19ym… nginx-linux.2 nginx:latest ubuntu-1 Running Running 2 minu…

3. Confirm the operation — you should get back the default NGINX landing page:

# curl localhost:8000
<!DOCTYPE html>
<title>Welcome to nginx!</title>

The swarm routing mesh took care of bringing back the response from either of the 2 containers.

4. Now we repeat the process, but this time launching a Windows-based service:

# docker service create — name iis-windows — replicas=2 — publish 80:80 — placement-pref microsoft/iis

5. Examine the service and note that the 2 Windows containers are on the Windows node:

# docker service ls
ID NAME MODE REPLICAS IMAGE PORTS ullugr7… nginx-linux replicated 2/2 nginx:latest *:8000->80/tcp
5tc80pl… iis-windows2 replicated 2/2 microsoft/i… *:80->80/tcp
# docker service ps iis-windows
ikhas… iis-windows.1 microsoft/… windows-1 Running Running 2 minu… xact2… iis-windows.2 microsoft/… windows-1 Running Running 2 minu…

6. Confirm the operation — you should get back the default IIS landing page:

# curl localhost:80
!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” …
<html xmlns=”">
<meta http-equiv=”Content-Type” content=”text/html; charset=iso-8…<title>IIS Windows Server</title>

There are still a few gaps in the Docker engine on Windows Server 2016. The biggest issues seem to revolve around networking to the containers. You probably noticed that I used a non-standard port for the Linux HTTP server mapping (8000) and the standard port (80) for the Windows.

If you research this issue, you will see many comments and issues going back into 2016, and it seems that they have not quite all ironed out yet. When I tried to use a different port in this example, it simply did not work. I got the service, the containers were there, and if I mined the internal IP assigned to the container on the Windows server, I was able to see the IIS landing page by using curl <internal IP> from the PowerShell prompt. However, it was not mapped to the swarm unless the same port was used on both ends. A couple of links that discuss the issues are below.

Despite these shortcomings, this example still demonstrates how a Windows and Linux container can both co-exist and provide a common serivce like a web server. Hopefully the networking stuff is addressed soon, enabling ‘full’ support for such activities. In the meantime, go forth and containerize!