Boost productivity with Visual Studio Code dev containers

Have you had situations when you may have many solutions or services, some which may rarely be changed, simply because they work as expected, or there are not much changes done to them?

Then suddenly, there is a change that should be implemented, or something breaks and you need to fix it as soon as possible? You need to figure out what software and releases to install, to being able to work at all on the problem…

Or you have a solution that is being actively worked on, but it may be tricky to set up all bits and pieces in the developer environment to become productive?

In this article, I will present one solution for such situations, the Visual Studio Code dev containers.

Introduction to dev containers

In situations like these mentioned above, the Visual Studio dev containers is a very nice feature. It allows you to have the tooling and settings in Visual Studio Code set up for you - all saved in the solution’s repository you should work with. All this is set up in a Docker container, so it does not affect your local installations. The container is also integrated with your Visual Studio Code environment.

Visual Studio Code itself is a quite capable editor, and very popular for doing development work.

How to get started

You need three things installed to get started on your computer:

  1. Visual Studio Code

  2. Docker Desktop

  3. The Visual Studio Code dev container extension

A project/repository which uses the dev container feature will usually has a .devcontainer directory in its root directory. Inside that directory there will be at least a file called devcontainer.json, which contains configuration information on how to set up a container image for the developer environment running as part of the Visual Studio code project setup.

It will also often contain a Dockerfile which describes how to build the docker image itself as well. Depending on the build image, there may also be other files.

Let us go through a step-by-step setup to create a dev container. Fortunately, the dev container extension makes this quite simple. First, we assume that you have a project (empty, or existing project) for which you want to set up a dev container environment, and you have the software setup in the list above.

1. Start the dev container setup wizard

In Visual Studio Code, select the command palette (View menu, then “Command Palette…”). Look for the command “Dev Containers: Add Dev Container Configuration Files…“ and select that.

Dev Container add config files

2. Pick a base container image

Pick “Show All Definitions...” to see all pre-defined container definitions available. There will be a long list of container images for different languages and tools. For our demonstration purposes, we will pick the “Node.js” image.

Pick Node.js image

You will then be prompted with the version to use for Node.js. Pick any fairly recent version (16, 18). If you use a Mac with the new Apple chips, pick the bullseye alternative.

3. Pick additional features to install

Next is a list of additional features to install to the container image. These are software packages for which there are pre-packaged build/install steps for some common tools, so you do not need to set up these yourself. In our example, we can pick AWS CLI, Docker-from-docker (makes your host Docker available inside the container), Git (if you plan to use git from the command-line) and Homebrew (for easy install of additional software).

Add dev container features

You will then be prompted for some version options for the features you selected as well. For now, pick any version you want - latest or a specific version specified.

4. Start dev container, or customize

Now the wizard part is completed and you can start to use the dev container, or do further customizations. You will get a prompt to re-open with dev container:

re-open in container

…and if you answer yes, it will build and run the container. The first time it may take a little while, while following starts will go quicker due to image data being cached.

Starting dev container

Inside the .devcontainer directory, you will find 3 files:

  • devcontainer.json - this is the dev container launch configuration. This includes things VS Code extensions to use, dev container features, docker build configurations, etc.

  • Dockerfile - the docker image build steps to execute

  • base.Dockerfile - this is for informational purposes mainly. This is the dockerfile definition that is the base image your Dockerfile is working from. You can use this if you want to tweak something for this base image.

dev container files

Next, we will look at a concrete example of customising the dev container setup.

Customizing the dev container

For this example, I am going to use one dev container I set up for myself to work with AWS CDK and Pulumi with Typescript. The starting point is faily similar to the steps above in the [[#How to get started]] section.

Here, I have started with the dev container Node.js image. Even though I am working with Typescript, I do not install that with the image, since both AWS CDK and Pulumi will make the project settings when I initialize a project with either of these tools. I just need Node.js, and yarn, which is included in that image as well.

For features, I have selected a couple:

  • AWS CLI - I am working with AWS, AWS CLI will always be useful to have

  • Docker-from-docker - Both Pulumi and AWS CDK may make use of Docker for some of their tasks and builds. Obviously, if you work with containers, but also with AWS Lambda sometimes.

  • Git - I use git from the command-line a fair amount, in combination with some operations via VS Code. So I want that one in place as well.

  • GitHub CLI - Some things towards GitHub I may use the command line for, others may go through VS Code, or sometimes the web interface. It is nice to have that in place.

  • Homebrew - This is a package manager for macOS and Linux, and a tool that makes many software installations quite simple. There are some software that I choose to install via Homebrew.

There are three more software components I want to install, which are not part of Dev container features, and which I will install separately through customising the setup:

I also want to map my AWS profile configuration into the container, as well as make my SSH keys accessible from the container.

The AWS profile and SSH configurations I make available in the container by adding an entry in devcontainer.json:

“mounts”: [
  “type=bind,source=${localEnv:HOME}/.aws,target=/home/node/.aws”,
  “type=bind,source=${localEnv:HOME}/.ssh,target=/home/node/.ssh”
],

Pulumi and Granted are installed via Homebrew. I also want to execute and package installations for the project I may work on, via Yarn. This is another entry in devcontainer.json.


“postCreateCommand”: “brew tap common-fate/granted && brew install granted pulumi && yarn install”,

For the installation of AWS CDK, I add that to the Dockerfile and install it via yarn there. I also add some settings for Granted, which it otherwise will prompt for during the first execution, which I want to avoid. This is to set up a config file with some settings, and a shell alias for the assume command that is part of Granted.

RUN su node -c “yarn global add aws-cdk”

COPY --chown=node granted_config /home/node/.granted/config
RUN su node -c ‘echo >>/home/node/.bashrc alias assume=\"source assume\"‘

The complete file set-up in .devcontainer directory includes three files:

  • devcontainer.json

  • Dockerfile

  • granted_config

The contents of the file are:

devcontainer.json

// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node
{
    "name": "Node.js",
    "build": {
        "dockerfile": "Dockerfile",
        // Update 'VARIANT' to pick a Node version: 18, 16, 14.
        // Append -bullseye or -buster to pin to an OS version.
        // Use -bullseye variants on local arm64/Apple Silicon.
        "args": { "VARIANT": "16" }
    },
    "mounts": [
        "type=bind,source=${localEnv:HOME}/.aws,target=/home/node/.aws",
        "type=bind,source=${localEnv:HOME}/.ssh,target=/home/node/.ssh"
    ],

    // Configure tool-specific properties.
    "customizations": {
        // Configure properties specific to VS Code.
        "vscode": {
            // Add the IDs of extensions you want installed when the container is created.
            "extensions": [
                "dbaeumer.vscode-eslint"
            ]
        }
    },

    // Use 'forwardPorts' to make a list of ports inside the container available locally.
    // "forwardPorts": [],

    // Use 'postCreateCommand' to run commands after the container is created.
    "postCreateCommand": "brew tap common-fate/granted && brew install granted pulumi && yarn install",

    // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
    "remoteUser": "node",
    "features": {
        "docker-from-docker": "20.10",
        "git": "latest",
        "github-cli": "latest",
        "aws-cli": "latest",
        "homebrew": "latest"
    }
}

Dockerfile

# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
#     && apt-get -y install --no-install-recommends <your-package-list-here>

# [Optional] Uncomment if you want to install an additional version of node using nvm
# ARG EXTRA_NODE_VERSION=10
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"

# [Optional] Uncomment if you want to install more global node modules
RUN su node -c "yarn global add aws-cdk"

COPY --chown=node granted_config /home/node/.granted/config
RUN su node -c 'echo >>/home/node/.bashrc alias assume=\"source assume\"'

granted_config

DefaultBrowser = "STDOUT"
CustomBrowserPath = ""
CustomSSOBrowserPath = ""
LastCheckForUpdates = 4
Ordering = ""
ExportCredentialSuffix = ""

Final notes

This is an example of setting up a development environment using Visual Studio Code dev containers. I have found this a useful feature, and above all, also accessible. There are other solutions which also provide the ability to set up targeted environments for development, but I have found the Visual Studio Code dev containers to strike a good balance between features and ease-of-use.

I do not use the dev containers for everything. In fact, I prefer to work with the JetBrains IDE tools if I can. But for several projects and cases which I may not touch daily, or others may need to work on occasionally, dev containers make it easier for people to get productive fast.