Building a Dev Container for .NET Core
How to create a full-time development container for .NET Core 3.1 with VS Code
In this article:
- Selecting VS Code Extensions for .NET Core development
- Creating a Dev Container
- Managing your Dev Container
- Integrating VS Code and Dev Container
While some developers are highly productive using text editors such as VI, VIM, Notepad++, etc. that they combine with command line tools, others prefer integrated development environments (IDE). In this article we’ll be using Visual Studio Code.
In addition to the IDE and extensions, I also use development containers via Docker Desktop (download here: https://www.docker.com/products/docker-desktop). Using development containers, or short “Dev Containers” comes with several advantages including:
- More consistent development environment between developers’ computers regardless of operating system
- Containers are more similar to cloud deployments if containers are used there as well
- Setting up a new development computer is simplified: install VS Code with a couple of extensions, development container, git client and you’re ready to go. This makes onboarding new team members much simpler
My work often requires me to work with different technology stacks, e.g. .NET, Ruby, Python, etc. In addition, I need to switch between database servers, e.g. SQL Server, Postgres, or between Azure or AWS. The combination of VS Code with development containers allows me to set up just the tools I want, and I can easily switch between technology stacks.
How to make all of this work is the content of this article. Although you can get images for readily made development containers on the internet, in this article we’ll create one from scratch. This will provide learning opportunities and it will create a better understanding of how things work, preparing us to adapt the setup in the future as we see fit. At the end of this article we will have a setup that enables development for .NET Core in a development container.
Before we start, let me clarify one more point. There are quite a few examples for using a container for specific development tasks. A large number of these examples run the container only for executing a task. Then the container terminates. In this article we’ll take a different approach: We will create a “full-time development container”, i.e. the development container will be used all the time rather than for the duration of a task.
In this and future articles we’ll be using as a running example a fictitious product named “Mahi”. The word Mahi means “task” in Te Reo Māori, the language spoken by the native people of Aotearoa, the country also known as New Zealand.
Mahi is a very simple task manager. We won’t have a commercially viable product at the end. But we will learn new concepts as we work through new features. Keep in mind, that the code base is not meant for production. You are welcome to use it for your own work, commercial or otherwise. The responsibility is entirely yours, though.
The complete source code for this article is available at https://github.com/mahi-app/CmdLine/tree/article-2020–10–26. Just make sure you select branch “article-2020–10–26”.
The list of prerequisites is fairly short. Here it is:
- A current version of a git client from https://git-scm.com/downloads
- Newest stable version of VS Code from https://code.visualstudio.com/download
- MacOS and Windows: Docker Desktop from https://www.docker.com/products/docker-desktop
- Linux: docker engine: follow instructions at https://docs.docker.com/engine/install/
To follow the remainder of this tutorial, all you need is a git client. Download the client for your operating system from https://git-scm.com/downloads and follow the installation instructions.
Configuring VS Code
Open VS Code. On the left-hand side of the VS Code window click on the “Extensions” icon in the Activity Bar. Note that you may have a different set of icons in the Activity Bar.
From the list of extensions search for and install the following extensions (versions as of time of writing):
- “Remote Development”, identifier “ms-vscode-remote.vscode-remote-extensionpack”. This extension may show as “Preview”. This is fine. The extension is stable enough to be used on a daily basis. (Version 0.20.0, preview)
- “GitLens”, identifier “eamodio.gitlens” (Version 10.2.2)
- “Docker”, identifier “ms-azuretools.vscode-docker“ (Version 1.6.0)
These are all the extensions we need for this article. This list of extensions is what I’d recommend regardless of technology stack. We’ll install more extensions later.
Initialize Directory as Git Repository
Initializing the git repository is usually one of my first steps. Create a folder “Mahi” in your preferred location for new projects. Then within “Mahi” create another folder named “CmdLine”.
Open a terminal in path “Mahi/CmdLine”. Then execute the command
to initialize an empty git repository at that location. I’ll assume for the remainder that you are familiar with the basics of git.
Creating the Dev Container
We will not cover all details of Docker, just enough details so that we can build and use a Dev Container for .NET Core.
What is a Container?
Essentially a “container” is an isolated environment to run a process. Both, the process and the outside world are protected from each other.
We’ll cover a few more Docker concepts in this article. For a more detailed introduction to Docker concepts, check out https://docs.docker.com/get-started/.
Containers and Images
To run a container, first it has to be built. As containers may be reused by many people, docker uses the concept of an “image”. A container image is similar to a template. We can create as many containers from an image as we like.
For example, we might have a container image that has a database server pre-installed. This image may have been created by someone else, e.g. the database vendor. We can simply download the image from what is called a “container registry”. The download is typically referred to as “pull”.
Docker operates a container registry at https://hub.docker.com. It offers a range of images including the one that we’ll use in this article. Registries, public and private, are also available from AWS and Azure.
Building a Container Image
There are two ways to create a container image:
- Using a “Dockerfile”: this file describes the steps to create a single container image (advanced usage allows creating more than one image, not covered here)
- Using a docker compose file: this approach uses one or more files to describe one or more containers that work as a group
An example of a group of images may be a web application that uses a database. We can build (or pull) all of the images and then start, stop and remove the containers as a group.
In this article we’ll use both approaches. It’s a bit overkill for a single container but we’ll add more container in future articles.
The Dev Container
Next, we’ll create our very own dev container.
Start VS Code and select “File” — “Open Folder…”. Then find and open the folder “Mahi/CmdLine” which we created earlier.
VS Code’s window title should now include the word “CmdLine”. On the Activity Bar click the “Explorer” icon:
The list of files should be completely empty at this point as we haven’t created any files yet.
Now that we have opened the folder in VS Code, our next step is to create the dev container. We’ll use a Dockerfile to build the basic image for our dev container. And we’ll use a docker-compose.yml file to run up the dev container with some parameters.
We’ll start with the Dockerfile for our development container. Within VS Code create a new directory named “dev” and then inside “dev” a file named “Dockerfile” without extension.
Please confirm that the icon next to the filename “Dockerfile” is a blue whale. If not, then you don’t have the “Docker” extension yet for VS Code. See section Prerequisites above for how to install it.
The contents of the Dockerfile is fairly small at the beginning. All we do is specifying which image we want to use as the base image and then we’ll create a non-root user for the container to be user during runtime. Running with minimum privileges is a good security practice here as well.
Since we are going to use .NET Core, we’ll use a base image that also includes the .NET Core SDK. In this case we will use an image that Microsoft has already created for us. It includes both the runtime as well as the SDK, which includes tools such as the compiler or debugger. By using the pre-built image, we avoid the work of installing the .NET Core SDK in the container. The first line in the Dockerfile specifies the base image using the keyword “FROM”:
No need to type this all in. You can easily grab it from the github repository at https://github.com/mahi-app/CmdLine/tree/article-2020–10–26.
In this case the image comes from Microsoft’s public container registry (MCR).
The second part of the dockerfile sets up a non-root user. We use a RUN directive that executes a command, in this case “useradd” to add a user named “mahi”. The option “-m” creates a home directory for the new user. Without going into further details, this home directory will be used for installing support for remote development, support for C# and similar more. The option “-s $(which bash)” sets bash as the login shell, i.e. the shell to be used once a user logs into the container.
With the second RUN directive we create a directory “/app” and change its ownership to “mahi”. Finally, the “USER” directive switches to the newly created user “mahi”. When we run a container from the resulting image, it will run as “mahi” instead of “root”.
This is all we need for now in the Dockerfile. Don’t forget to commit and push this change.
Docker Compose File
The second element we need is a file named “docker-compose.yml” next to the existing “Dockerfile”. Here is the entire content of this file (also available in the git repository):
This file is available at https://github.com/mahi-app/CmdLine/blob/article-2020-10-26/dev/docker-compose.yml.
Let’s walk through the contents of this file, one item at a time.
The first line indicates the file format version. We’ll use ‘3.7’, one of the more recent versions available as of writing. More information about compose file versioning is available at https://docs.docker.com/compose/compose-file/compose-versioning/.
Next in the docker-compose file is the list of services, starting at line 3. A “service” in this context is a process running in a container. For this article, we need only one such service. We’ll call it “cmdline-dev” (line 4).
Next, we tell Docker how to build the image (lines 5 and 6). In this case we just provide the “context”, which is essentially the working directory for “docker-compose” in case the container image doesn’t exist yet. At that location docker-compose will find the file named “Dockerfile” and uses it to build or update the image.
With parameter “working_dir” (line 7), we tell docker-compose which directory inside of the docker container should be used as the working directory once the container runs. We’ll use “/app” which is an absolute path in Linux (the Dev Container itself will be a Linux-based container). If you are a Windows user note that a Linux file system starts at “/” and that there are no drive letters.
The next parameter specifies the “volumes” to be used by the container (lines 8 and 9). This value requires a little more explanation. What happens here is that we map a directory on the host — “..” in this case — to a directory within the container’s file system — “/app” in this case. In other words, a directory of the host becomes accessible from within the container, similar to sharing a folder.
Since docker-compose.yml is inside the folder “CmdLine/Dev”, this means that “..” refers to the folder “CmdLine”, i.e. the root of our repository (it also contains a “.git” folder). That location will be accessible inside the container at path “/app”. This mapping is called a “mount” in Linux terms.
Finally we need to make sure that the development container doesn’t terminate immediately. We use the “command” parameter for that and set it to “sleep infinity” (line 10).
Now that we have both, the dockerfile and the docker compose file in place, we can run our first few experiments.
Open the root folder of the repository in VS Code, then open a terminal window in VS Code. In that terminal switch to directory “CmdLine/Dev”, then execute the following command:
This command builds container images as per the instructions contained in the file “docker-compose.yml”. Since we have listed only one service in the file, this command creates one image. You should see output similar to the following:
In the Activity Bar of VS Code switch to the docker tab. Then look for the image named “dev_cmdline-dev”:
Then, in the terminal window in “CmdLine/Dev” execute the command:
docker-compose up -d
This command starts the services listed in the file “docker-compose.yml”. The option “-d” runs the containers in the background, i.e. the terminal remains usable. As you executed this command you should see output similar to the following:
We can ignore the line referring to “network” for the time being. Once the container is running, we can list running containers by using the command
Executing this command should list something similar to the following:
The container has a unique id, “ffde98aa5bd0” in this case. The output also lists the name of the image used for the container, the command executed, when it was started, the status and the names of the container. The column “PORTS” is empty but will become important in future articles.
Next, execute the following command:
This command uses the default file again, i.e. “docker-compose.yml”, in the current container to stop the container(s) listed and remove them. You should see output similar to the following:
Again, we’ll ignore the entry “Removing network dev_default”. We’ll cover this in a future article.
We now have created the files needed to build, run and stop the development container. Up to here, we didn’t us anything that is specific to VS Code. In the final part of this article, we’ll look into how to use the development container in combination with VS Code.
Integrating VS Code and Dev Container
The last part of this article will demonstrate how to integrate VS Code with the development container we just created.
VS Code uses the extension “Remote Development” for working with containers. If you haven’t installed this extension yet, please see section “Prerequisites” above.
By convention the extension tries to find one of the following (note the use of the dot-prefix):
- A configuration stored in a file named “.devcontainer.json” at the root of the repository, i.e. in the folder that you open with VS Code
- A configuration stored in a file at “.devcontainer/devcontainer.json”
We’ll use the second option for this article as it keeps the VS Code specific files separated from the remainder of the files in the repository.
The content of the file “devcontainer.json” is relatively simple:
This file is available at https://github.com/mahi-app/CmdLine/blob/article-2020-10-26/.devcontainer/devcontainer.json
The first parameter is self-explanatory. Next is an array of docker-compose files to be used to build and start the development container. In this case it is the “docker-compose.yml” file we created previously in directory “/dev”. If you have more than one docker compose file (not covered in this article), then keep in mind that the order matters. Content files listed later overwrite the same setting from a file listed earlier.
The parameter “service” specifies which service VS Code should attach to as its development container. Remember that Docker creates one container for each service. This service name must appear in one of the docker-compose files listed in this “devcontainer.json” file. Here we use the only service we have created earlier. We named the service “cmdline-dev”.
“workspaceFolder” provides the directory to be used as the work directory inside of the container. In this case we just use the same we listed in the file “docker-compose.yml”.
Next is a list of extensions. Here I have listed some suggestions for tools that I found useful for .NET Core development in C#. Once you’re familiar with the environment, please feel free to adjust this as you see fit.
The fact these extensions are listed in “devcontainer.json” illustrates how you can pick a different set of extension for each repository, i.e. for each development container. In this case it is for .NET Core using C#. But you could as easily use extensions for other tech stacks or tasks. Obviously, we’d consider using a different base image in those cases instead of the .NET Core SDK one.
The last parameter “shutDownAction” is used to tell VS Code what to do when it closes. In this case, we use “stopCompose” which means that it stops the containers and removes them (one in our case).
And this is all we need. Let’s see how we can use this.
Open Folder in Dev Container
Close VS Code. Start VS Code again and open the older “Mahi/CmdLine”. This time we are prompted with a dialog box informing us that the folder supports opening the workspace in a container.
In case you missed the dialog box, no problem. We choose “View” — “Command Palette…” and type in “remote-containers”.
Then we click the entry “Remote-Container: Open Folder in Container…” which then starts the Dev Container using the information in the file “.devcontainer/devcontainer.json”. If required the Dev Container will be built before it is started. In particular in the latter case loading may take a while to complete. If the Dev Container has already been built, opening the workspace in the Dev Container is similarly fast than without it.
Once the workspace has been loaded, this is shown in the bottom left corner of the VS Code window:
To demonstrate that we are ready to get started with .NET Core development open a terminal window inside of VS Code: “Terminal” — “New Terminal”. In that terminal execute the command
The output should look similar to the following:
If you like you can also try the command
to see information about the linux distribution that the dev container is using or
to see the content of the current directory.
And that is all there is! We are ready to get started with our first project in a Dev Container.
We’ve covered a lot of material in this article, so the first project in a Dev Container is a story for another day.
I hope you found this article valuable for your work as a software engineer. Thank you for reading!