Getting Started With Docker – An Introduction
Today’s header image was created by chuttersnap at Unsplash
Before we begin: this post is part one of a series of posts. I’m not sure how long they’ll all be, but I know that there’ll definitely be more than one part to this, as I want to introduce what docker and containerisation is all about, before jumping into the nitty-gritty of how to dockerise your .NET Core apps.
My current plan is to do four blog posts:
- Introducing docker
- Dockerising an ASP.NET Core application
- How docker works
- Optimising docker images
As such, if you’re looking for an article on how to use docker with .NET Core apps, then you’ll have to wait around for part two. I’d still give this post a read through though
and check out the section at the end for some resources and stuff
because there’s bound to be something in here for you.
What Is Docker?
It will have been quite difficult to be interested in .NET Core and ASP.NET Core
remember, they’re two different things
without having heard much about docker, containers or DevOps. Especially since the DevOps revolution has been in full swing for a good few decades. But what is docker? It’s an application which makes it easy to implement containerisation.
Ok, so let’s talk about what containerisation is first.
This is something which goes all the way back to 1979. It has it’s roots in Unix V7’s chroot and FreeBSD’s Jails
you can read more about the history of containers here
The basic idea is that you take your application and everything which is required to run it, and place it in a walled garden (sometimes called a Closed Platform). The reason for this is simple: you describe everything that your application needs in order to run, and lock it down so that the outside world cannot alter it
That way, if you want to install your code on a new system, you just spin up a new instance of the container and hey presto
you don’t have to say “hey presto”, but it helps
your code is up and running, with everything it needs.
So docker does that.
How Does It Work?
I won’t get into the nitty gritty of this, as there are many, many other folks who have done a much better job than I.
But, essentially what docker does is it manages a running instance of the Moby project. Your application is booted, as a process, by the running instance of Moby, and docker deals with forwarding requests to and from your application.
Imagine it like this:
- Moby is an APM Transfer Terminal
like in the header image for this post
- docker is the security guard
- Your application is in an Intermodal Container
You can’t see whats inside the freight container, and the only person who is allowed to look is the security guard. You pass a message (a request) to the security guard and they take it to your container. The container then figures out what to do with that message
the analogy is already breaking down, but bear with me
and it can respond to the message, via the security guard.
Sure, it’s not the best analogy ever, but it’ll do. Essentially, what docker does is the above (really bad) analogy.
What Is It Used For?
Remember back in the day
or maybe right now
when you had a bunch of site running on the same server, but with different port numbers assigned to them via a reverse proxy like IIS?
What if someone malicious got access to your server? They could change the files which were being served by IIS to be whatever they wanted.
Well, docker takes that power away because your app is hidden away in Moby.
it’s still possible to alter a running docker container, but it’s not as easy as changing a file on disk
But the bigger bonus for using docker is that it removes the dreaded “it works on my machine” excuse that most devs have when QA (or worse, real users) raise an esoteric bug.
Most of the time, strange bugs are caused by platform differences. This will likely be because the Live server may not match the UAT server, which also may not match the QA server, which definitely wont match your development machine.
One way to solve this is to containerise your application, that way it’s running on the same platform, no matter whether it’s on QA, UAT or Live.
you do have different environments, don’t you?
Because the application is running in exactly the same platform set up, regardless of where it’s running, you can recreate esoteric bugs a lot easier. You can also scale your application really easily, too.
With a more traditional deployment model, you’d have to separate your application from it’s data store (i.e. the database)
you’re doing this already, aren’t you?
and have multiple instances of your application running on many different servers. Then you’d have to have another server (traditionally called a Load Balancer) sit between your application servers and the real world, which would redirect all incoming requests to one of your application servers. As soon as one of those servers became overloaded, your load balancer would have to redirect requests to one of the redundant servers.
What makes docker so brilliant is that your load balancer can be used to spin up on-demand instances of your application, on whatever server is available, as more requests come in. This is, essentially, how Netflix works: They have a bunch fo load balancers as edge servers. They then have containerised micro services which can be spun up to deal with all requests.
Netflix are famous for using containerisation, in fact there’s a story relayed in The DevOps Handbook
by Gene Kim, Jez Humble, Patrick Debois & John Willis
about how well Netflix was able to survive a major hardware outing, during peak user time, due to their micro service design and containerisation:
An interesting example of resilience at Netflix was during the “Great Amazon Reboot of 2014, when nearly 10% of the entire Amazon EC2 server fleet had to be rebooted to apply an emergency Xen security patch. As ChristosKalantzis of Netflix Cloud Database Engineering recalled, “When we got the news about the emergency EC2 reboots, out jaws dropped. When wee got the list of how many Casandra nodes would be affected, I felt ill.”
Of the 2,700+ Casandra nodes used in production, 218 were rebooted, and twenty-two didn’t reboot successfully. As Kalantzis and Bruce Wong from Netflix Choas Engineering wrote, “Netflix experienced 0 downtime that weekend.”
Even more surprising, not only was no one at Netflix working active incidents due to failed Cassandra nodes, no one was even in the office – they were in Hollywood at a party celebrating an acquisition milestone.
Kim, G., Debois, P., Willis, J., Humble, J. and Allspaw, J. (n.d.).The DevOps handbook. pp.280-281.
That quote is from a section which was primarily about resilience exercises and constant feedback
which is one of the core tenets of the DevOps way
but it shows of just how well your apps could respond when leveraging a container and orchestrating technology (like kubernetes, for example).
Why Not Use A VM?
Putting aside technologies like Vagrant for a moment, have you seen the size of a standard VM image?
Let’s assume that you want to run your .NET Core application in a Windows Server 2016 image, that would require a 32 GB image on disk. And that’s before you start thinking about how much hard drive space, CPU time and RAM that your host operating system needs.
And then you need to check that binary blob into source control
you do use source control, right?
so that your engineering team can pull down the image and get it set up on their machine or on a live server.
Plus, running a live server which has multiple massive VMs installed and running on them can get very expensive – especially if you want to use a cloud service
in fact, Azure don’t allow you to run VMs on a VM in the cloud
Most source control systems aren’t very good for managing changes in a binary blob. Which also means that we’ll have issues when someone needs to make a minor change or tweak to the VM.
It can be done, but it’s not easy to manage.
However, docker supports the use of so called dockerfiles. These are plain text files (so they’re great for managing in a source control system), and are usually quite small. As an example, the dockerfile for PokeBlazor
which was submitted by the amazing Joe Zack of the Coding Blocks podcast
is 871 bytes. In fact, here it is in its entirety:
|# Stage 1: Compile and publish the source code|
|FROM microsoft/dotnet:2.1-sdk-stretch AS builder|
|COPY *.sln ./|
|COPY PokeBlazor.Client ./PokeBlazor.Client|
|COPY PokeBlazor.Server ./PokeBlazor.Server|
|COPY PokeBlazor.Shared ./PokeBlazor.Shared|
|COPY global.json global.json|
|## restore onto a separate layer. That way, we have a single|
|RUN dotnet restore|
|RUN dotnet publish --configuration Release --no-restore --output /app/out /p:PublishWithAspNetCoreTargetManifest="false"|
|# Stage 2: Copies the published code out to published image|
|ENV ASPNETCORE_URLS http://+:5000|
|COPY --from=builder /app/out .|
|# Super hack to work around https://github.com/aspnet/Blazor/issues/376|
|RUN mv -n wwwroot/* PokeBlazor.Client/dist|
|RUN rm -rf wwwroot/|
|ENTRYPOINT ["dotnet", "PokeBlazor.Server.dll"]|
Once you’ve had a little experience with dockerfiles, reading them will be second nature as they’re super simple to read.
Why Not Vagrant?
A very similar thing can be done with Vagrant .Except that Vagrant is designed to allow users to describe the resulting run time environment. For instance, the following vagrant file
created by GitHub user theparticleman and sourced from here
shows how to set up a minimal .NET Core runtime environment using an Ubuntu base image:
|# -*- mode: ruby -*-|
|# vi: set ft=ruby :|
|Vagrant.configure(2) do |config||
|config.vm.box = "ubuntu/trusty64"|
|$script = <<-SCRIPT|
|sh -c 'echo "deb [arch=amd64] http://apt-mo.trafficmanager.net/repos/dotnet/ trusty main" > /etc/apt/sources.list.d/dotnetdev.list'|
|apt-key adv --keyserver apt-mo.trafficmanager.net --recv-keys 417A0893|
|apt-get install dotnet -y|
|config.vm.provision "shell", inline: $script|
However, as the instructions for the file show, you need to take more steps in order to get an app up and running:
vagrant up, do a
vagrant sshto ssh in to the VM. Once in, run the following commands:
dotnet new– this sets up a new “Hello World” project
dotnet restore– restores nuget packages
dotnet run– runs the project
again, the source of this vagrant file can be found here
Whereas, the above dockerfile does four things:
- Sets up a build environment
- Builds the source code
- Sets up a runtime environment
- Runs the application and exposes a port number
From my point of view, docker is easier to get up and running with in a shorter amount of time. And you can use it to describe your build environment, which means that you can describe all of your build and runtime environments with one file.
And this is especially useful if you end up leaning towards DevOps, which almost every developer will.
How Do I Dockerise My Application?
The tl;dr version is:
very carefully, and with a lot of thought about how the application works
The fuller version of this answer is that you would, ideally, dockerise it from the very start of development. Otherwise, how do you know that you’re build and runtime pipelines actually work?
I’m going to go into this in a lot more detail in part two
so look out for that
in the meantime, it’ll be worth checking out some of these links for more information or resources on docker or containerisation: