Upgrading a Dev Container to .NET 5.0
By: Manfred Lange
Tags: Net5, Vscode, Net Core, Docker, Devcontainer

Improving the Dev Container

Upgrading the .NET SDK to .NET 5.0

As a first step for improving the dev container, we’ll be upgrading the .NET SDK to version 5.0. This is relatively easy.

To get started, open a bash shell and switch to the local clone of the repository. For example, if you used the steps in “Prerequisites”, then the following command should bring you there:

cd ~/projects/CmdLine

Once there, execute

code .

This will launch VS Code. If prompted to re-open the directory in the dev container, accept the offer and do so. Wait until docker is finished and VS Code has installed all extensions in the docker container.

Our first step will be to confirm which branch we are on the correct branch. Execute the following command:

git branch

This should output “* article-2020–11–15” (note the asterisk). If this not what you are seeing then switch to the branch with the starting position using this command:

git checkout article-2020–11–15

Then confirm with “git branch” that the switch was successful.

If you like you can create a new branch in your local clone and commit to it as you read this article. Be aware, though, that you won’t be able to push to the clone on github.

Our next step is replacing .NET Core 3.1 with .NET 5.0. In fact, we will be even more specific: we are going to use the .NET SDK version 5.0.101. This is the latest stable release as of writing. Because we are using a dev container, this upgrade does not require installing the new framework version. Instead, we will just use a new base image for our dev container. The new base image has the SDK pre-installed.

Within VS Code open the file “dev/Dockerfile”. In it replace the first line with the following code

FROM mcr.microsoft.com/dotnet/sdk:5.0.101

With this, one would think, everything is fine. However, that is not the case as we will see shortly. To see what the problem is, we’ll rebuild the container by clicking on the green corner at the left end of the status bar:

Rebuilding the Dev Container in VS Code

Wait until the container has been re-built. This may take a while longer if the base container image needs to be downloaded. Note, that we will be rebuilding the dev container a few times. You can always refer to this section in case you forgot how to do that.

One VS Code has rebuilt the container, open a terminal in VS Code and execute the following command:

ps

This will produce an error similar to the following:

Command ‘ps’ not found

While we may not need that command for development tasks, some VS Code extensions depend on it and don’t work correctly or not at all if ps is not available. Therefore, let’s fix this problem, which is easy to do.

Again, open the dockerfile and just after the “FROM” directive add the following two lines. Make sure to include the backslash at the end of the first of these two lines.

RUN apt-get update && \

apt-get install -y procps

The first few lines of the Dockerfile should now look as follows:

Adding missing “ps” command in Dockerfile

Again, click in the status bar in the bottom left corner and choose “Remote-Containers: Rebuild Container” once more. Wait until everything is finished and then try the “ps” command in a terminal window inside of VS to confirm this now works as expected:

Command “ps” successful

To confirm that we are in fact now using the .NET 5 SDK, run “dotnet — info” in the terminal window in VS Code. You should see output similar to the following:

dotnet — info for .NET 5.0

We have successfully changed the .NET SDK to .NET 5.0. Well done!

Managing Group and User

There are a couple more things that we should improve with our dev container. One of them is reviewing how we configure the non-root user for the dev container. Remember: for security reasons it’s a best practice to use the dev container as a non-root user only. This ensures that processes and commands running inside of the dev container do not have root permissions. What is more important, this also applies to all mounted directories (which in turn may allow access to even more commands, directories and files). In fact, the non-root user won’t have sudo available either which plugs that potential hole as well.

In Linux each user is a member of a group. Both the user and the group have a number by which they are identified. An example would be a number like 1000. Give this a try by running the command “id” in a terminal window in VS Code:

Running “id” in the dev container

This command gives you information about the current user, i.e. “mahi” in this case. The id for the group and the user are both 1000 in this case. We didn’t control this. This number just happened to be the next one to use when we added the user using “useradd” in the Dockerfile. Also, the value for “groups” lists the groups the user is a member of. In this case this is just “mahi” (but not “sudo”).

We want to be specific about the user and group id. For this to happen, replace the line

RUN useradd -m -s $(which bash) mahi

with the following two lines:

RUN groupadd -g 1001 -r mahi && \

useradd -u 1001 -r -g mahi -m -s $(which bash) mahi

In this instance we specify the user id and the group id as the number 1001. The first few lines of the Dockerfile should now look as follows:

Rebuild the container for this change to take effect, then check the output for “OmniSharp Log”. You should find an error similar to the following:

Issues with Permissions

This particular issue is related to the user VS Code is using to connect with the remote vscode-server. In this case VS Code is using the user and group id of the user that launched VS Code. Let’s check that out.

In the bash terminal that we used to start VS Code with command “code .” execute the command “id”. In my case the output looks as follows:

Output of “id” on the host (or in WSL2)

As you can see the user id (“uid”) is 1000. Therefore, if VS Code uses this user to attach to the dev container, then “access denied” errors are not really a surprise. Let’s be specific about what user VS Code connects as. Reopen the folder locally (Windows only: in WSL if using WSL).

Open the file “.devcontainer/devcontainer.json” in VS Code and add the following line

“remoteUser”: “mahi”,

to the configuration as follows:

Specifying the user for the dev container

Then reopen the folder in docker. When we check the “OmniSharp Log” we notice that the error is still present. So, this was not enough just yet. We need more modifications: we need to ensure that the user we connect as owns the directory with the clone.

Adding entrypoint.sh

Reopen the folder locally again (Windows only: re-open in WSL if using WSL). Next, we’ll move changing the file ownership using “chown” from the Dockerfile to a script named “entrypoint.sh”. Also, we’ll remove the “USER” directory from the Dockerfile.

The reason for moving “chown” has to do with the fact that first the container is built and started using the Dockerfile and docker-compose. Only, then is mounting the volume complete, i.e., mounting of the clone into the dev container. We can “chown” the director and its contents only, after the mounting has completed. Therefore executing “chown” in Dockerfile is too early in the process. By moving it to a later stage we’ll make this more robust.

Apply the following lines

# Create working directory. Ownership will be changed in entrypoint.sh which

# executes *after* the volume has been mounted.

RUN mkdir /app

# Copy entrypoint script into container, make it executable, then execute it:

COPY entrypoint.sh /entrypoint.sh

RUN chmod +x /entrypoint.sh

ENTRYPOINT [“/entrypoint.sh”]

to the Dockerfile:

What we do here is this: In line 16 we create a directory “/app”. This will become the mount point for the repository clone. Then in line 19 we copy a file named entrypoint.sh into /entrypoint.sh. This is at the root of the file system so anything we do with “/app” won’t interfere.

In line 20 we change the permissions with “chmod” to make the script “/entrypoint.sh” executable. Note, that at this point of the Dockerfile commands are still executed as the root user. The “ENTRYPOINT” directive in line 21 tells Docker to execute “/entrypoint.sh” once the container is up and running.

Next, we’ll create the script “/entrypoint.sh”. We’ll create this file at “dev/entrypoint.sh”. It has the following content:

#!/bin/sh # Change ownership of all directories and files in the mounted volume, i.e.

# what has been mapped from the host:

chown -R mahi:mahi /app # Finally invoke what has been specified as CMD in Dockerfile or command in docker-compose:

“$@”

Or in VS Code:

There is not much content in this file. In line 5 all we do is changing the ownership of the directory “/app” and all of its content to “mahi:mahi”. (the first is the group name, the second one the user name). In line 8 we just execute what might be specified with the CMD directive in the Dockerfile or as value for the “command” parameter in the docker-compose file.

Let’s revisit the reason why we moved the chown command from Dockerfile to the entrypoint.sh script. As mentioned before, the Dockerfile is used to create the container image. Then docker-compose.yml tells Docker to mount the host directory at “/app”. Only once this has completed, we can reliably chown the directory “/app”.

Another option would be to use the “USER” directive in the Dockerfile to switch to user “mahi”. However, if we did, we couldn’t execute “chown” in the script “/entrypoint.sh” as “chown” requires root privileges. User “mahi” does not have those privileges by design.

So, to clarify the chronological order of events, here are the main steps as they are executed when the dev container is started:

If the container image doesn’t exist just yet, the instructions in Dockerfile are executed as “root” to create the docker image. When we run up the dev container, the content of “docker-compose.yml” is used to orchestrate the containers we want, two in this case. This includes mounting the parent directory on the host, i.e. the root of the repository clone, at “/app” inside the dev container (see lines 8 and 9 in the docker-compose.yml file). This part is still executed as root. Still running as root, and once the container is up and running, Docker executes the script specified by the “ENTRYPOINT” directive in the Docker file. This script changes the ownership of the directory “/app” inside the dev container to “mahi:mahi”. Next, the “command” specified in docker-compose.yml is executed. We still run as root. Once all of the preceding steps are complete VS Code considers the container to be up and running. At that point VS Code connects as user “mahi” (configured via “remoteUser”, see above) and installs “vscode-server” and then all other extensions that may be configured for the dev container. “vscode-server” and everything it spawns inside the dev container runs as “mahi” including any terminals that may be open, just as we wanted.

In a VS Code terminal you an experiment in the dev container. Try using “sudo” or accessing directories or files that are owned by “root”. You will find that user “mahi” has no root privileges.

Choosing a Better User Id and Group Id

As described above, we used the number 1001 for the user id and for the group id for non-root user “mahi”. There is one problem, though, with this. To see that problem, go to the bash terminal from which we started VS Code and run the following commands:

cd ~/projects/mahi-app/CmdLine

ls -lart

This should give you an output similar to the following:

Notice how the group is listed as 1001 and the user as docker. If we were to edit any of the files listed or any files in those directories then you’ll notice that we don’t have permission to do so. The reason is that your own user id and group id is different. Execute the command “id” in this bash terminal. This should give you output similar to the following (your user name will be different):

Notice that the user id (“uid”) and the group id (“gid”) are 1000 (the number may be different in your case). Because this number is different to the number 1001 that we assigned to user “mahi” and to group “mahi”, Linux will prevent us from editing these files.

We can fix this by running “sudo chown .”. However, we wouldn’t want to run this all the time. Also, as soon as we re-open this in the dev container, it will be changed again back to 1001. There is a better option. We will change mahi’s user id and and group id to match the id of the user as which we run on Linux outside of the container.

In my case, it’s the user “manfred” and the group “manfred” and both have the number 1000. Let’s use these numbers in the Dockerfile when we add the group and the user:

If in your environment the command “id” shows a different number for the group and/or the user id on the host, then use those numbers instead.

With this change in place, rebuild the dev container once more. Then, in the bash terminal on the host (the terminal from where you launched VS Code), run “ls -lart” which gives you something similar to:

Running the same in the dev container gives us the following:

Even though we see different user and group names the underlying ids for group and user are the same as we have mounted the host directory into the dev container. We wouldn’t need the “chown” command in this case. Howerver, the “chown” command in script “/entrypoint.sh” ensures that we have sufficient permissions in the dev container. If you want to confirm that the ids are the same, just run the “id” command both in the dev container as well as on the host’s bash terminal.

Note that in the last picture the parent directory (“..”) is still owned by root. This is exactly what we wanted. We use the dev container as user “mahi” and have permissions only in the directory “/app” (and mahi’s home directory) but nowhere else. This means if any malicious code should manage to execute within the dev container, it will be limited to what is accessible inside of “/app” of the dev container and to what is available in the home directory (“~”). We haven’t mounted anything elsewhere, so the home directory is limited to the dev container as well.

Furthermore, sudo is not available in the dev container and even if it was, the non-root user is not a member of that group. And even all of that was available, then you’d still have to manually enter a password to execute sudo.

Now, that we have strengthened the security of our dev container by using it as a non-root user, let’s next look at another improvement that allows us maintaining the nuget packages we use in our project.