Docker

Deploy a .NET Core API with Docker


In this how-to, we create a Docker image based on a .NET Core API, deploy to DockerHub, and run on Windows, Linux and Azure.

What is Docker?

Docker is a containerization platform, meaning that it enables you to package your applications into images and run them as “containers” on any platform that can run Docker. It negates the classic: “It works on my computer” argument as Docker images contain everything needed for the app to run.

Are Containers the same as Virtual Machines?

They are similar, but not the same. In short:

  • Virtual Machines provide OS Level Virtualisation
  • Containers provide App Level Virtualisation

This concept is depicted below:

Containers Vs VMs

Why Would You Use it?

In short you would use Docker, (and hence containers), for the following reasons:

  • Portability. As containers are self-contained they can run on any platform that runs Docker, making them easy to stand up and run on a wide variety of platforms.
  • Scalability. With the use of additional “orchestration” you can spin up multiple container instances to support increased load.
  • Performance. Containers generally perform better than their VM counterparts.
  • Ubiquity. The level of Docker adoption in industry means that it’s a great skill to have.

Installing the Prerequisites

In order to follow along with this tutorial you’ll need:

I’ve provided links to all of the above, where you should be able to find not just the software, but install instructions for your respective operating system.

NOTE: Windows users at install time be sure to leave the default as “Linux Containers”, (as opposed to selecting Windows containers).

Additionally, if you want to follow along with the cloud deployments, you’ll need accounts on:

  • Docker Hub (you need this if you want to pull to a local environment too)
  • Azure

With our prerequisites installed, and any optional cloud hosting accounts set up, let’s move on to creating our app.

Create Our App

We’re only going to create an out the box .NET Core API app based on the webapi project template in this guide. If you’d like a full .NET Core API build guide however, I have covered that in this article.

I’m going to take a highly manual approach to everything in this article, (e.g. VS Code & .NET Core CLI), as I feel that lays better foundations for learning. Once you’re comfortable with the concepts by all means use the “wizard” functionality in Visual Studio to streamline your use of Docker.

So to create our API app, open a command prompt and type:

dotnet new webapi -n SimpleAPI

This will create an API project called “SimpleAPI”, you should see output similar to the following:

Project Created

Start VSCode and select: File -> Open Folder and select the “SimpleAPI” project folder that was just created in the last step:

VS Code Folder View

SimpleAPI project opened

Open the Startup.cs class, and remove the following code from the Configure method, (this just makes the testing of the app a little bit simpler – in a production app you may want to consider keeping this code though):

Startup Code Removal

So all you should have in the configure method now is:

app.UseMvc();

Save the file, and at a command prompt, change “into”, the SimpleAPI project directory, (listing the contents of the folder you should see):

Project Directory Listing

At the command prompt type:

dotnet run

This will start the app on localhost on ports 5000 & 5001 for http and https requests respectively:

Running App

Now open a browser, and enter the following URL:

http://localhost:5000/api/values

This “hits” the following API Controller action:

GET Values Controller Action

And displays the JSON object as shown:

Displayed Result

That’s all we’ll be doing with our API app, so if that didn’t make much sense and you want a more detailed explanation on how to put a .NET Core API together, check out this article.

Images & Containers

Before we set up our app to run in Docker, you should be familiar with 2 terms that you’ll hear frequently when talking about Docker: Images and Containers.

Images

A docker image is a file that contains the “blueprint” or instructions on how our code is expected to run in Docker, so it includes things like dependencies and instructions on how our app should start.

Container

When our image is “executed to run”, in runs in a container, which as we’ve already discussed is highly portable and independent, (from any other running containers).

Analogy with OO Development

When I starting out with Docker I made the following analogy with Object-oriented software development, (as I was more familiar with that):

  • Image = Class
  • Container = Object instance

While this comparison may not be 100% accurate, I feel its close enough to be useful. I.e. you can take 1 image and spin up multiple containers instances…

So the first thing we need to do to get our app running in Docker is to create an image, we do that by introducing a “Dockerfile” to our project.

The Dockerfile

The Dockerfile basically defines the image we are going to create, a simplified view of the workflow is depicted below:

Simple Dockerfile Workflow

In short, we’ll

  • Define our Dockerfile
  • Using the Docker CLI will issue a “build” command
  • The Docker engine will parse the file
  • The Docker engine will create an image ready for use

So lets get started and create our Dockerfile. The first step is to add a file named “Dockerfile” in the root of our project, (note that the file has no extension):

Dockerfile added

If you’re using VS Code it may ask you to install an extension that will give you Dockerfile syntax parsing, (or IntelliSense), as well as some other useful features – I recommend you install it:

Docker Extension VS Code

Now in your Docker file add the following sections, we’ll go through what’s happening at each step below:


# Get Base Image (Full .NET Core SDK)
FROM mcr.microsoft.com/dotnet/core/sdk:2.2 AS build-env
WORKDIR /app

# Copy csproj and restore
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Generate runtime image
FROM mcr.microsoft.com/dotnet/core/aspnet:2.2
WORKDIR /app
EXPOSE 80
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "SimpleAPI.dll"]

Referring to the line numbers below, I’ll explain what’s happening at each step next:

Dockerfile example

  • Line 2: So that the Docker Engine can compile our app, we grab the .NET SDK from Microsoft*
  • Line 3: We specify a dedicated “working directory” where our app will eventually reside
  • Line 6: We copy the .csproj file from our PC to the working container directory (/app)
  • Line 7: Using dotnet restore we resolve any project dependencies (this is done using the .csproj file and retrieving any additional dependencies via Nuget)
  • Line 10: We copy the rest of our project files into our working directory, so we can build the app
  • Line 11: We run the dotnet publish command, specifying that it is a Release build, (-c Release), as well as specifying a folder, (out), to contain the app build dll and any support files & libraries.
  • Line 14: To keep our image “lean” we retrieve only the  aspnet run time image, (as opposed t the full SDK image we used for building), as this is all our app requires to “run”.
  • Line 15: Re-specify our working directory
  • Line 16: We expose the port we want to use from inside our app
  • Line 17: Copy the relevant files from both the dependency resolution step, (build-env), and build step, (/app/out), to our working directory /app
  • Line 18: Set the entry point for the app, (i.e. what should start), in this case it’s our published .dll using “dotnet”.

* The Docker Engine builds up an image in stages by using an empty container at each stage and working inside that. Therefore we have to assume that the container environment is “empty”, hence why we need to pull down the .NET SDK base image to allow for the compilation of our app.

Save Docker file. Additionally, to minimise the footplaint of our image, you should include a 2nd file in the root of our project called: .dockerignore again note there no extension. Add to this file the following contents:


bin\
obj\

Save this file and we’re ready to move to the next step – creating our image!

Create Our Image

With the Dockerfile saved, it’s time to “build” our image, this is relatively easy with the Docker CLI. Just before we do that though, one point worth mentioning is the naming convention of Docker images. The standard format structure is detailed below:

<Docker Hub ID>/<Project Name>:<Version>

So in my case I’ll call my Docker image:

binarythistle/simpleapi

You’ll note that I didn’t provide a <Version> component, in which case the Docker engine will default it to “latest”. You can of course provide your version numbering if you want to.

Also, you don’t have to provide your Docker Hub ID in the name* but it’s useful if you’re pushing it up to the Docker Hub for deployment.

* Not only do you not have to provide a Docker Hub ID, but you don’t even have to name or “tag” an image at all. In this case the image will just be generated with a unique id.

So, further discussion aside, at a command prompt enter the following, making sure to replace “binarythistle” with your own Docker Hub ID, (or leaving it out all together).

Note: Make sure that you place a space and period after the name – it’s easy to miss!

 docker build -t binarythistle/simpleapi .

The output of issuing this command is shown below, (you can see how it interprets the Dockerfile build steps), resulting in the build and “tagging” of our image:

Docker Image Build Steps

This image will be added to our Docker Image cache, to see the available images on your system type:

docker images

or

docker image ls

In my case I get the following, (your output will look different):

Docker Image Cache Listing

With our image created, we move onto running it!

Run On Localhost

Running on our local machine is easy, issue the following command :

docker run -p 8080:80 binarythistle/simpleapi

Your image should now be running as a container! The only thing of note is the “-p” flag – this is a port-mapping, in this case it’s saying map port 8080 on my PC to port 80 of the container. So to access the api, we need to use port 8080 as follows:

http://localhost:8080/api/values

This will map through to the “Exposed” port 80 specified in the Dockerfile, you should see the same output as before.

Now open a new command window and type:

docker ps

This will show all the currently running containers, you should see the following:

Running Containers CMD

Or if you’re using VS Code and installed the Docker extension, you can see running containers, stop that container etc…

Docker in VS Code

To stop the container from the command line type:

docker stop <CONTAINER ID>

Where the <CONTAINER ID> is the id displayed when we listed the running containers above, (it is not the image name – a trap I fell into a few times!), e.g.:

Stop Docker Container

Or, if your using VS Code with the Docker extension, you can right click any running container and select stop from the menu.

So that’s all great, but you’re probably asking so what? We had the app running natively on our local machine, and all we have done is “containerise” and run it, drum role, on our local machine, (all be it as a container).

And to be honest with you, if we were only ever going to run the API on our local machine and nowhere else – it would be pretty pointless. The real power of Docker is when we come to deploy it elsewhere, and the ease with which that can be achieved.

To do this though, we need to publish our image somewhere so others can use it.

Push to Dockerhub

Note at this point you need to have signed up for a Docker Hub Account if you want to follow along with the various deployments.

What is Docker Hub?

Docker Hub is simple a repository where you can find a library of useful Docker Images, these can be from full-on software vendors, or individuals like me or you. Browsing over there, (https://hub.docker.com), and clicking “Explore” on the main menu you will see some of the most popular images available for download:

Popular Images

Or if you look at my personal space, you can see some of the, (test), images I have available:

Personal Images

Indeed it’s to this space that we’re going to publish, (or push), the image we’ve just created, (or in your case, your own personal space).

To push your image to your Docker Hub, we first need to login to Docker Hub, so at a command prompt type:

docker login

Now, here you may be asked to provide your Docker Hub ID, (note this is not your email address, but the ID you selected when you signed up), and password. However I believe as I’m signed into Docker Hub via the desktop client, I don’t have to provide this:

Docker Login

To push the image simply type:

docker push <Image Name>

Where <Image Name> is, amazingly, the name of your image, which in my case is: binarythistle/simpleapi:

Docker Push

This will push the image up to your repository space:

Image Pushed To Docker Hub

Cool! Our .NET Core API image is now available for consumption, let’s now go through how to run our image on a few popular hosting endpoints.

Pull To Other Hosts

Linux

So before we move onto Cloud deployment with Azure, I thought we’d run our little app on a new, (this time Linux), box with nothing installed on it but Docker. Just to prove the point that containers are fully self-contained, independent deployable apps.

Note: you can really follow the same steps at the command line on any OS that has Docker running and you should get the same result.

Over on the Linux box, (I’m using Ubuntu), at a command line type:

sudo docker run -p 8080:80 binarythistle/simpleapi

As this image is public, Docker should go to Docker Hub, (as it can’t find the image locally), pull down the image and run it. Indeed it’s really no different from the run command issued against the local image in the previous example, (except Linux required the sudo command to be added):

Linux Running

Again browse to:

http://localhost:8080/api/values

And our container app will return with our JSON object!

Firefox in Linux

Azure

Login to the Azure portal, (it goes without saying that you need to sign up for an account), and click on “Create a resource”:

Disclaimer: Azure is a paid for service, so you can ultimately accrue charges on your subscription. Following the example below should incur minimal to zero charges assuming you delete the resource when done. However I can’t be held responsible for any charges you do incur though!

If you’re worried about spiralling Azure costs, check out this article on how to monitor and budget your Azure subscription.

Anyway back to the tutorial…

Azure - Create a Resource

In the resulting search box type “Container Instances”, this should be available for selection from the drop down, so select it!

Container Instances

You’ll see some information about what a Container Instance is, (it’s quite self explanatory), so hit “Create:

Container Instance 2

In the basics screen enter the relevant details:

Container Instance - Basics

  1. Select you subscription
  2. Select a “Resource Group” if you don’t have an existing one, (say this is your first time on Azure), then you’ll need to create one.
  3. Give your container a name, this is how it will be identified in Azure, it doesn’t need to be the same as your image name.
  4. Select the Azure region you want to host it in
  5. My image is public so I have that selected
  6. Enter the name of your image, this why the naming convention is so important, so it can be uniquely identified
  7. I want to run a Linux-based container, (you can of course select Windows if you like)
  8. Select the sizing options, as we’re just testing the defaults are fine

Once your happy click “Next: Networking >”

I’ve left the defaults and just entered a DNS name label – this is how we’ll browse to the API over the internet:

Azure Networking

We don’t need to configure anything else so click: “Review + create”, which takes you to the Review Screen, if you’re happy click “Create:

Azure Review

Azure should then go off and find your uniquely tagged image on Docker Hub and start to build our instance, this will take a few minutes:

Azure Deploy

You’ll eventually be notified the deployment is complete, when so, click on “Go to resource”:

Deploy Complete

Looking at the resource, you’ll see some details and metrics on how it’s performing, copy the FDQN as we’ll paste this into our browser next:

FDQN

Having pasted the FDQN, (Fully Qualified Domain Name), into your browser be sure to append:

/api/values

Then navigate…

Azure Deployed

Boom! Our .NET Core API is running in a container on Azure – how easy was that?

Back over in Azure you can Stop or even delete the Container Instance resource when you no longer need it.

NOTE: You may incur charges on your Azure Subscription if you leave it running.

Wrap Up

The focus of this tutorial was to illustrate the ease with which you can deploy apps in Docker, (once you learn the basics of Docker of course!)

Thanks for reading – see you next time!

Docker
Connect to SQL Server running in Docker