This blog post first appeared on my blog
I have been hearing a lot of people talking about docker and all of the benefits it introduces and they got me interested in the technology. However, I only recently managed to get some time to play around with Docker.
What I attempted to do was to take a DotNet Core WebApi project that I'm building and containerize it. The WebApi project was build initially with no intention of containerizing and it was surprising how easy it is to get it up and running in docker.
The project also uses a Microsoft SQL database and for me, this was a bit challenging to setup and get the WebApi to communicate with it properly.
In this post, I'll explain the steps I took to set it up with sample dockerfiles and docker compose configurations.
To get started, you'll need to install docker on you development machine. I'm using a Windows 10 Machine. You can read the install instructions here
The dockerfile will take care of setting up the image for the WebApi project. If you don't know what docker images are, I strongly suggest reading the Docker Getting Started section here.
In this dockerfile we will execute the same commands we do on our machine when building and publishing any dotnet core application via the dotnet cli.
Usually we do this (at the root of the project):
> dotnet restore > dotnet publish -c Release -o out
then when we inspect the out directory in the project root, we will find the compiled assemblies of our project and we can then simply execute this command to run the application/webapi:
> dotnet MyProjectEntryPointAssembly.dll
But there are a few extra command we will execute in the dockerfile to setup the base image and the entrypoint for the resulting image.
First, let's look at my project's folder structure:
Let's look at my dockerfile and I'll explain what every command is doing:
FROM mcr.microsoft.com/dotnet/core/sdk as build WORKDIR /app COPY src/. . RUN dotnet restore RUN dotnet publish -c Debug -o out FROM mcr.microsoft.com/dotnet/core/aspnet as runtime WORKDIR /app COPY --from=build /app/webapi/out ./ EXPOSE 80 CMD [ "dotnet", "webapi.dll" ]
Ok, let's start with the first line:
FROM mcr.microsoft.com/dotnet/core/sdk as build
This is telling docker that we are going to use the DotNet Core SDK image from microsoft as our base image. You can find the image here in Docker Hub.
The ... as build is an alias given to a build stage. This makes it easier when you want to reference a build stage from another one as it is more friendly to work with names. By default docker doesn't name build stages. You can still reference build stages that don't have an alias defined by using then integer number which starts from 0 for the first build stage.
The next step in the dockerfile is to set a working directory within the image we just pulled from docker hub. The working directory name can be anything and docker will create that directory if it doesn't already exist in the base image. I chose to name my working directory as App but feel free to use whatever name you like. To set a working directory, we execute this command:
Next, we need to copy our project files from our local dev environemnt to the base image. You can think of the base image we just pulled as a Virtual Machine (they aren't actually VMs but this might help when working with them) that docker will initialize and run so it won't have access to our files. We copy the files to the base image by executing this comand:
COPY src/. .
This is copying everything inside the /src directory into the working directory we specified earlier.
Ok, so now that we have all the source code in the base image we're ready to build it. Since we are still inside the DotNet Core SDK image, the dotnet command is available in the shell, so we will tell docker to execute a couple of dotnet cli commands for us to build the project:
RUN dotnet restore RUN dotnet publish -c Debug -o out
The RUN keyword here is telling docker to execute the following commands in the shell of the base image. So this will restore nuget packages and then performs the publish command and outputs the results to the out directory. Remember, we are still in the work directory we specified so everything is happening under /app, our code is in /app/webapi/. and the outout folder will be in /app/webapi/out/.
Awesome! now that we have successfully got our code to compile and publish (inside the base image) we will move to the next build stage.
The reason we have multiple stages in this dockerfile and the reason to use them (at least in this case) is to avoid creating big images.
The Microsoft DotNet Core SDK base image is about 400MB. While we can build the project and also run it inside this image, using ~400MB + ~75MB for my webapi project seems like a waste when we can instead use the Microsoft DotNet Core Runtime base image which is much smaller in size to run my webapi.
To start a new stage in the dockerfile we use the FROM statement. For this stage, we will pull the Microsoft DotNet Core Runtime image found here by executing this command:
FROM mcr.microsoft.com/dotnet/core/aspnet as runtime
we name this stage as runtime and we set its working directory also to /app:
Next step we need to grab the build outout files from the previous stage and copy it over to the currect stage. We do this by executing this command:
COPY --from=build /app/webapi/out ./
The --from=[stage name] is specifying the source of the files to be from another image/stage. /app/webapi/out is the source of the files in that step and ./ is the destination (the root of the working directory in this step).
Once the copy is complete, and since this is a webapi project that will run on port 80, we need to tell docker that we port 80 should be expesed to incoming network traffic:
Simple, way too simple!
The final command we run is literally running our webapi. To do this we execute:
CMD [ "dotnet", "webapi.dll" ]
Now that we have a complete dockerfile we can test it by using the docker cli to build the final image and running it.
To do that, open a terminal, and set the working directory to the root of you project (where the dockerfile is located) and run:
docker build --tag=MyWebApiProject .
the build command will execute the commands in the dockerfile we created. --tag=[name] gives the resulting image a name. the period after the command sets the build context to the current directory. docker will attempt to find the dockerfile in the current context and all the commands inside the dockerfile will run within that context. This is important for when we copy the files to the SDK image as we are using relative paths. All relative paths will rely on the build context we set.
If your build is successful, you will have a docker image stored on your machine.
Use docker image ls to list the images on your machine. You can see the image you created and it's size.
On my machine, the SDK image is 1.74GB but the webapi image is only 283MB because it is using the runtime image as per the multi-stage dockerfile above.
Now it is time to run the containerized application and this is as simple as:
docker run -p 5000:80 MyWebApiProject
docker run is the CLI command to run a containerized application's image.
With -p [OS port]:[Container port] I'm are binding a port on my operating system (in this case port 5000) to the port we exposed earlier in the dockerfile (port 80). This binding will allow us to use HTTP REST Clients such as Insomnia to send HTTP requests to the webapi application in the container.
That's it! This is how I took an existing and under development DotNet Core WebApi project and containerized it in docker.
In the next part, I'll explain how to setup a containerized MS-SQL server and create a docker swarm to have my WebApi project talking to the containerized MS-SQL server.