Building a docker image with Gitlab CI and .NET Core
Nowadays, there is a heavy focus on scalability and containerization but also automatisation. As developers, we want things to be setup once and with the click of a button (or sometimes not even), push our application to production and be done with it. The first step to achieving this is being able to create a Docker image from a CI Pipeline.
I thought this would be an interesting topic for me to write about as it’s a rather important step to achieving Continuous Deployment and perhaps not the most straightforward.
Getting started
I’m going to keep it as simple as possible and will try to explain most things as well as I can so there is not much required except:
- Basic knowledge of docker
- Basic knowledge of Gitlab CI (see my other post)
- Have a basic understanding of how .NET Core projects are built
The project structure we will be working with is:
example
└───src
├───example.sln
└───Example.Presentation
├───Controllers
└───...
It’s just a simple webapi
project, generated with dotnet new webapi
.
However, by default, dotnet new
does not generate a .sln
file so make sure you create one in the /example/src/
folder as the pipeline we will be creating assumes there is a solution file in that location.
Setting up our pipeline
Our pipeline will basically build the docker
image through a Dockerfile
and then push it to our Container Registry on Gitlab.
We will make sure we steer clear of using any keys, credentials, tokens, etc. inside our pipeline to prevent them from being stolen.
Let’s add a .gitlab-ci.yml
file in our /src
directory and get started.
Our image will be docker:stable
:
Next, we will have to define a service named docker:dind
. DinD stands for “Docker inside Docker”. We need this as our Gitlab Pipeline is basically a docker container, therefore our docker container will have to spawn another docker container. We call this Dockerception. (no clue if anyone actually says that 🤔).
So now with that out of the way, we can get into the actual script.
Before doing any jobs I like running the docker info
command which might help when you’re debugging the pipeline:
Let’s create a build
stage which only runs on the master
branch.
So now for the actual building of the image.
Before we run any docker commands, we want to log into our registry.
We want to be able to do this without directly using credentials inside the pipeline, we will make use of Gitlab’s predefined environment variables. Here, we only make use of a fraction of the available variables but you can find a list of all of them here, the names can change sometimes so make sure your gitlab instance is up to date if it’s self-hosted.
Let’s add a docker login
command. We have to pass the URL of the Gitlab registry, our username and password.
As you can see; we’re logging into registry.gitlab.com
without providing any hardcoded credentials, exactly as we want!
So now.. building and pushing the image to our registry!
It’s rather simple, really. We just have to run docker build
and docker push
.
We want to push it to the registry specific to our repository in Gitlab. This is located at registry.gitlab.com/{USER}/{REPOSITORY}, of course, there is an environment variable already defined for all this.
So we run a build with the-t
or —-tag
flag, basically giving a name to our image. Then we push it to our Gitlab Registry
.
Now, we log out and tell the runner to run the build
stage.
And.. boom! We’re done with setting up our pipeline!
But.. we’re not quite done yet. docker build
requires a Dockerfile
which defines all the steps required to build the image.
The Dockerfile
A Dockerfile
is used by docker to build an image. It has all the steps in it required to build the image, from setting the base image to building the project. It can be a bit hard to get at first but I’ll run through and explain every step as best I can.
Creating the Dockerfile
Simply create a Dockerfile
in the same directory as the gitlab-ci.yml
and we can get started :)
Base image
The base image we will be using is microsoft/dotnet:2.2-sdk
. If you’re reading this a while from now, you might be using a different version of .NET Core, in which case you can find the latest image here.
A base image can be used so you don’t have to manually install all packages (.NET Core SDK in our case). Using images from the Docker Hub is not always the safest as there might be security flaws etc. In our case it’s okay but if you’re going to deploy to production you might want to make sure it’s secure and ready for production or create your own base image.
Temporary Container
This is not our final container, its base image is the dotnet SDK, which is not what we want when we push our project to production. However, we do need the SDK in order to build the project.
Later on, we will create one based on the dotnet runtime. This will become more clear in a bit.
Copying *.csproj files
We want to restore our packages now, as you know, all this is saved in the .csproj
file so we will have to copy each one to our docker image.
In this example there is only 1 project but if you have multiple in your own project, you will have to copy those too.
Below, we first run a WORKDIR
command. This simply changes the directory to /source
. If the directory doesn’t exist, it’ll create it.
Afterwards, we run COPY ["{Location inside gitlab-ci}", "{New location inside Docker image}"]
. This copies our file to the image.
Restoring packages
Now that the *.csproj files are copied, we can run a dotnet restore
on our solution’s entrypoint. Generally, when running dotnet restore
on the presentation layer, packages for all files will be restored.
In a Dockerfile, you can run a command by prefixing it with RUN
.
Afterwards, we copy all our source code into our image (to our current directory, the /source
folder, remember?). It might look a bit weird but it’s basically doing the same thing as when we copied our .csproj files. COPY {location inside gitlab-ci} {new location inside docker image}
Building the project
It’s very simple here, we change our WORKDIR
to /source/src
. This is because we copied all our files into the /source
folder, meaning we have to move into the /src
folder to publish the project.
Then, we run a dotnet publish
command. The exact command may vary for your specific project but we’re simply publishing it with --configuration Release
and the output folder as /publish
. Simply stuff ;)
Creating the final container
Now, our project is published but it’s using an SDK image, which is not what we want. So we will define a new base image, set the WORKDIR
to /publish
, copy the published files to the new image and set the entrypoint of the project to our Presentation layer.
Now our Dockerfile is finished and so is the pipeline.
If you push a commit, the pipeline will be executed. Make sure your Registry is enabled as this might not always be enabled by default.
Now, when you go to the Container Registry in the sidebar of your gitlab repo, you can see that an image has been created and pushed to the registry.
Conclusion
We went through all the steps of setting up a Gitlab CI Pipeline and building a docker image. Not the most difficult thing to do but definitely a very interesting topic.
I hope this post provided you with a good amount of knowledge to start creating and expanding your own pipeline.
The example project can be found: here.
Any questions or feedback is always very much appreciated!
Follow us on Twitter 🐦 and Facebook 👥 and join our Facebook Group 💬.
To join our community Slack 🗣️ and read our weekly Faun topics 🗞️, click here⬇