Setting up a VS Code Dev Container
Tags: vscode, docker, environment, newbie
Published: 2021-03-22 18:38:51

Last week my team was tasked with picking up a project that I had last worked on 4 years ago. The deadline was tight, and we had a week to complete it. It was written in PHP, a language I'd barely written in the last 3 years. Obviously a lot has changed in 4 years (both in the language, setup, and the team I work in). I am the only engineer in my team now who knows PHP.

My first reaction was to get a working environment that I could use, and then share with my team. OK, maybe that wasn't my first reaction 😅.

This post outlines how to setup a [VS Code Dev Container](https://code.visualstudio.com/docs/remote/create-dev-container), so your team can share the same development environment.

## Why use a Dev Container?

I am not a fan of the "Works on my machine" mentality. I've [blogged](https://benmatselby.dev/post/development-environments/) about this before. Engineers need to work in a consistent environment for their project. Ideally this would be closely aligned to the environment you ultimately ship to, i.e. Production.

We don't want engineers wasting time on maintaining language versions, or OS libraries, or anything that slows them down. We want that burden to be taken away from them, or at least shared amongst the team. If you're in a team that works on multiple projects, keeping everything in sync will become a burden.

Dev Containers provide the perfect solution in VS Code. With just a few files, you can configure VS Code to have a consistent environment (including plugins) for all your team.

## Setting up a Dev Container

As a pre-requisite, you will need the ["Remote - Containers" extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) from Microsoft.

To setup a Dev Container environment, you have two options:

1. Manually write the files required.
1. Get VS Code to take you through the configuration screen and create the boilerplate files for you.

For your first attempt, I'd recommend option 2. Irrelevant of the option you pick, make sure you commit the `.devcontainer` folder to your version control system.

### Manually

You will require two files (which we go over in more depth later in the post):

- A `Dockerfile` defining your environment. If you're new to Docker, you can read [this](https://benmatselby.dev/post/docker-101/), or the [official documentation](https://docs.docker.com). The definition is whatever you need it to be, with no special requirements for VS Code. For example, you could base it on `node:latest` and add some extra packages if you wish.
- A `devcontainer.json`. This helps configure the VS Code environment, for example naming the environment.

### Using the configuration screen

Open your project in VS Code, and run the Command Palette (On the mac this is on the "View" menu or `shift+command+p`). This will allow us to Type "Remote Containers" and get a list of options.

We want to pick "Add Development Container Configuration Files..."

![Dev Container Base Options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1tg5uy5zu80kf5woduwr.png)

This will give us some standard Linux OS options, or you can select "Show All Definitions..." to get a more specific configuration, such as Go, Java, Node.js, Python, PHP etc. There are many options, so select the one that closely aligns to what you want.

![Dev Container Base Options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ctb4xxsp29e7nkt3ayxd.png)

For my project, I needed PHP, but obviously you pick what you require. In the PHP setup, it then asked me what PHP version I wanted, and if I wanted Node.js installing too. I selected PHP 7, and to install Node.js.

![Dev Container PHP Version](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4hc1co6asrj4enu3551a.png)

![Dev Container Install Node](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rydvmjdq6dr3q13vvote.png)

Once you have finished selecting your options, VS Code will open the `devcontainer.json` file, ready to edit.

## The Dockerfile

The Dockerfile is dropped into the `.devcontainer` folder within your project. If you have used the VS Code configuration screen, you're likely to be using a [base image provided by Microsoft](https://github.com/microsoft/vscode-dev-containers/tree/master/containers).

They have done a great job of annotating the `Dockerfile` with touch points you can override.

You really are free to do whatever is required here. I simply added the following:

```dockerfile
# Install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
  && apt-get -y install --no-install-recommends \
  vim git libzip-dev unzip fzf software-properties-common

# Install PHP modules
RUN docker-php-ext-install zip

# Install gh
RUN  apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-key C99B11DEB97541F0 && \
  apt-add-repository -u https://cli.github.com/packages && \
  apt update && \
  apt install gh
```

This is just to show an example, clearly your packages and dependencies are likely to be different.

## The devcontainer.json file

This settings file let's you configure VS Code and the way the container runs. It's named `devcontainer.json` and is also dropped into the `.devcontainer` folder in your project. For comprehensive documentation on the structure and values, please refer to the [official docs](https://code.visualstudio.com/docs/remote/devcontainerjson-reference).

For completeness, this is what I've addded:

```json
{
  "name": "my-simple-project",
  "build": {
    "dockerfile": "Dockerfile",
    "args": {
      "VARIANT": "7",
      "INSTALL_NODE": "true",
      "NODE_VERSION": "lts/*"
    }
  },
  "containerEnv": {
    "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
    "GITHUB_USER": "${localEnv:GITHUB_USER}"
  },
  "runArgs": ["--name=my-simple-project"],
  "settings": {
    "terminal.integrated.shell.linux": "/bin/zsh",
    "php.validate.executablePath": "/usr/local/bin/php",
    "editor.tabSize": 4
  },
  "extensions": [
    "bmewburn.vscode-intelephense-client",
    "davidanson.vscode-markdownlint",
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "felixfbecker.php-debug",
    "ikappas.phpcs",
    "ms-azuretools.vscode-docker",
    "timonwong.shellcheck",
    "tyriar.sort-lines"
  ]
}
```

Some key call outs:

- The `build` section allows us to pass arguments to the Docker build process, making this really flexible.
- `containerEnv` allows us to share environment variables from the host machine into the container.
- `runArgs` are passed to the Docker `run` command. Here, we have named the container, so it's slightly easier to `exec` into it, outside of VS Code, should we want to.
- `settings` allows us to define consistent VS Code settings for the project.
- `extensions` allows your team to agree on plugins required for the project.

## Dotfiles

Everything discussed so far brings project alignment for the teams working on it. That's great news, but engineers also like to be individuals. For example, to be productive, I want my shell aliases, my exports, my functions, my prompt, my oh-my-zsh plugins etc.

After a bit of searching I found that you can define where your `dotfiles` are stored and VS Code will install them for you. There are three settings you can set, or override.

1. Where your dotfiles are (`dotfiles.repository`).
2. Where they should be cloned to in the Docker container (`dotfiles.targetPath`).
3. How to install them (`dotfiles.installCommand`).

Luckily for me, my dotfiles are in [GitHub](https://github.com/benmatselby/dotfiles), and you can install them via an `install.sh` script, which turns out to be the default in VS Code.

Once you configure these options, you will get your `dotfile` experience in the VS Code container. This, for me, is the knock out feature that tips this solution over the edge. Consistency and individuality in the right places. Excellent.

## Load the project

Having said all of the above, you can now load your project in VS Code.

There are two ways of doing this:

1. Load VS Code, and it _should_ ask you to re-open the project in a container, if it finds the `.devcontainer` folder.
2. If not, you can open the Command Palette, and run "Remote-Containers: Rebuild and Reopen in Container".

The first time you do this, it may take a little while as it needs to build the Docker image. After the first load, each time you open the project, it should be much quicker (Unless you change the `Dockerfile`).

You are now using the Dev Container in VS Code. When you open the terminal, you are inside the container. Plugins are running inside the container too.

## Conclusion

Much like I was sold on [Vagrant](https://www.vagrantup.com) about 8 years ago, I'm now completely sold on VS Code Dev Containers. It feels natural, and more light weight than Vagrant in some ways. The `dotfiles` aspect was pure polish for me. What a great feature, and well implemented too. For folks who like control over their host machine, this makes for a cleaner dev experience too, as all configuration and setup is inside a container. And of course, it is project scoped.

Why don't you take some time to setup a Dev Container on your current project, and see if it helps you out?

## See also

- [Development environments](https://benmatselby.dev/post/development-environments/)