I had some free time recently, so I wanted to go deep into Docker (rather containerization).
Prerequisites
A development computer running:
- Visual Studio (mine is v15.5.2)
- Docker for Windows. Get Docker CE for Windows (stable). My current version is v17.12.0-ce-win47 (15139)
So what is Docker? While you must read through this article, for developers Docker is primarily a platform/manager to automate the deployment of your application inside a containerized environment. The main target of Docker is to create portable, self-sufficient containers from any application (think of a node.js app or asp.net core app or a windows service kind of app or a python app, anything you can think of). In this example, we shall be working to build a Docker image from a .net core console App and deploy it on your development machine and run it. Also, I shall be using docker for Windows, but same can be done using Linux environment too.
Just before you start you should also have a look at this article (and/or this), which articulates the difference between VMs and Containers.
Once your installation is done, you can run the 'docker info' command from Power Shell or Command prompt to verify the version (+ a few other important details) of Docker running on your machine.
Make sure you are running 'Windows Containers'. Right-click on the Docker icon in taskbar and check if it says 'Switch to Linux containers...', which means that you are using windows.
Let's create a basic .net core console app in Visual Studio that will print random characters in console output constantly.
Now we have a .net core console app that we can build and run on local development machine. Press F5 and you can see the output of the program. Now next is to publish this awesome app. Pretty easy, just run 'dotnet publish' command in PS/CMD (inside the project folder) and your package shall be ready in no time. Just to make sure all is good, you can fire up a PS/CMD prompt inside the publish folder and execute 'dotnet DockerConsoleTestApp.dll' to make sure your app is working fine.
Note: I could have created this package inside a container too, but for now to simplify, I choose to create it on my development machine only. You can read more on how you can use docker as a development platform, not only a deployment platform.
Now let's create an image for this world class app using docker so that later I can package it inside one or more containers & run it almost anywhere without thinking of any further dependency on environment. Let's add a docker file in our solution and paste the content below. To read more about this file look here. Shall explain in details about all the lines mentioned in that file.
- FROM microsoft/dotnet:2.0.4-runtime-nanoserver-1709 AS base
-
- WORKDIR /app
- COPY /bin/Debug/netcoreapp2.0/publish/ .
-
- ENTRYPOINT ["dotnet", "DockerConsoleTestApp.dll"]
Let's understand the lines written in the docker file. To start with I need a runtime to run a .net app, right? So how do I first build an image that will have .net runtime so that I can later port my code inside that image to make it executable. Thankfully you don't need to create your own. Microsoft (and all other technology providers already created these image for you and you just need to download from a proper repository, in this case that's hub.docker.com. Turns out that if you go here, Microsoft has an image already made for you with .net runtime inside it. Let's use the same then.
- FROM microsoft/dotnet:2.0.4-runtime-nanoserver-1709 AS base
The above line tells docker, go get an image from docker hub named 'microsoft/dotnet' and with tag as '2.0.4-runtime-nanoserver-1709' and create a container out of it. Also name it as base (naming this is not mandatory in this case though, you will see this in future post). Now how to choose a tag is important but for now lets skip that and choose the latest one available for .net core 2.0.
This line tells docker to create another container (just remember most of the steps you write in a docker file actually creates a different different containers as needed) on top of earlier container from previous step and create a folder called 'app' inside the new container's base path (for windows it's 'C:\' drive) and make sure 'app' is the current directory inside the container. Docker may or may not delete the previous container depending on various dependencies and to boost performance on next deployments.
- COPY /bin/Debug/netcoreapp2.0/publish/ .
Now, the above line tells Docker to copy all files from our publish folder inside the current container's working directory, which is still 'C:\app'. We are using relative path as the docker file is residing in the same folder along with the csproj file of the console app.
- ENTRYPOINT ["dotnet", "DockerConsoleTestApp.dll"]
The last line tells docker to execute a command inside the latest created container. The command is, well without any surprise 'dotnet DockerConsoleTestApp.dll' as you executed earlier to verify your published app.
That's it, we are ready to see this in action. So fire up a PS/CMD inside your project directory (where the dockerfile also resides) and run 'docker build -t alphaimage'. This tells docker to execute the docker file commands one by one (as explained earlier) and create/build an image named as 'alphaimage' (name has to be all lowercase). You can optionally add a tag too like 'docker build -t alphaimage:v1'. If nothing is provided as tag, 'latest' is considered. So at the end of execution you will see how all the steps described above are performed by docker engine and it creates the image you wanted. remember if the base dotnet image is not there in your local registry, docker will first download the same from docker hub. But later when you modify your code and run the same command again to build new image; docker will intelligently skip all downloaded images/sections.
To make sure that you have the image ready, run 'docker images -a' and check the output. You will find a few intermediate images too that were used to build the final image.
Now you have the image that contains the super critical app you just build. So let's run it inside a container and validate. Execute the 'docker run --name alphacontainer alphaimage:latest' command from PS/CMD and voila, you can see your app's output. Press Ctrl+C to stop the execution.
So what just happened with the command? We asked docker to create a container called 'alphacontainer' from the image called 'alphaimage' that has tag as 'latest'. Now if you remember the image already knows about the start up path, so after creating the container docker automatically started the app. For fun lets run the command again with a different container name, lets say ''docker run --name alphacontainer2 alphaimage:latest''. So now we have two containers running the same app using my development machine's OS as its base.
to get all running container details execute 'docker ps -a' and you should see below. You can see both the containers are still running (the app). When we pressed Ctrl+C it only stopped to display the output in the PS window.
Now imagine you had an app that is listening to Azure Service Bus and processing messages constantly. Just how easy to scale up the app with docker. This is just the starting point, scale up and down is a completely separate topic though :)
Now all is good and I can see the app is still running. Let's examine a bit inside the container. To get details about it, you can run 'docker inspect alphacontainer' and it should provide you some important details like the start up point, networking etc. Also to check whats inside the container like how the app is being used inside) you can always fire up a CMD inside the container to inspect the folder structure etc. This can be done as the base dotnet image Microsoft provides comes with the command prompt installed. To run CMD inside the container please execute this 'docker exec -it alphacontainer cmd'. This tells docker to fire up CMD inside the container (remember our app is still running inside it). You should now see a black CMD window running inside the container and by default it should have the working directory as 'app' as we specified earlier in the docker file. You can check all the files that got copied by executing 'dir' command. You can even go back to 'C:\' drive & validate the container's file system.
Now you can play with the running container, you can stop the container, start it again (in interactive mode or not with -i parameter) and when done remove it. You may want to remove the image too if not needed anymore.
- # you must stop before removing a container
- docker stop alphacontainer2
- docker rm alphacontainer2
- docker stop alphacontainer
- docker start -i alphacontainer
- docker stop alphacontainer
- docker rm alphacontainer
-
- #you must remove all containers before removing an image
- docker rmi alphaimage
Now just for a bit more fun, I'm going to remove the infinite loop in the console app and re-package the app and follow the same method to create the same image.
- static void Main(string[] args)
- {
- var i = 0;
- while (i < 10)
- {
- Thread.Sleep(2000);
- Console.WriteLine(GetLetter());
- i++;
- }
- }
Now, to run it in interactive mode without me naming a container, we can execute 'docker run -it alphaimage' command. Docker will create a container with a random name and execute the same. Remember now my program is not running constantly, it will exit after a certain time. So when you check the status of the newly created container, you will see it's already exited (provided the while loop is complete by then :)), not like the previous case where the containers were always in a running state until we stop it. You can actually add a -rm flag along with the above command that will make sure Docker removes the container it created after the execution is complete, 'docker run -rm -it alphaimage'.
Hope you enjoyed the article. Let me know any feedback you have.