Deploy .Net6.0 Web api with docker
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 Virtualization
- Containers provide App Level Virtualization
This concept is depicted below:
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 article you’ll need:
- .NET 6.0 SDK
- VS Code text editor, (you can use anything you like but I’d recommend this)
- Docker
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.
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)
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 going to create an .NET6.0 API app based on the webapi project template in this article.
I’m going to take a highly manual approach to everything in this article, (e.g. VS Code & .NET6.0 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:
Start VSCode and select: File -> Open Folder and select the “SimpleAPI” project folder that was just created in the last step:
Open the Program.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):
So all you should have in the configure method now is:
app.MapControllers();
Save the file, and at a command prompt, change “into”, the SimpleAPI project directory, (listing the contents of the folder you should see):
At the command prompt type:
dotnet run
This will start the app on localhost on ports 5000 & 5001 for http and https requests respectively:
Now open a browser, and enter the following URL:
http://localhost:5000/api/values
This “hits” the following API Controller action:
And displays the JSON object as shown:
That’s all we’ll be doing with our API app, so you can access all the end points from the browser.
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).
Comparison 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:
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):
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:
Now in your Docker file add the following sections, we’ll go through what’s happening at each step below:
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-envWORKDIR /app
# Copy everythingCOPY . ./# Restore as distinct layersRUN dotnet restore# Build and publish a releaseRUN dotnet publish -c Release -o out
# Build runtime imageFROM mcr.microsoft.com/dotnet/aspnet:6.0WORKDIR /appCOPY --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:
- Line 1: So that the Docker Engine can compile our app, we grab the .NET SDK from Microsoft*
- Line 2: We specify a dedicated “working directory” where our app will eventually reside
- Line 5: We copy the .csproj file and other project files 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 9: 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 12: 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 13: Re-specify our working directory
- Line 14: Copy the relevant files from both the dependency resolution step, (build-env), and build step, (/app/out), to our working directory /app
- Line 15: 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:
docker3387/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 “docker3387” 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 docker3387/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:
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
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 docker3387/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:
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.:
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
Or if you look at my personal space, you can see some of the, (test), images I have available:
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:
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: docker3387/simpleapi:
This will push the image up to your repository space:
Now, Our .NET6.0 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 docker3387/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):
Again browse to:
http://localhost:8080/api/values
And our container app will return with our JSON object!
Comments
Post a Comment