blog.hipsquare.net

Exploring Devcontainer in VS Code

Cover Image for Exploring Devcontainer in VS Code
Michael Herzner
Michael Herzner

Container and Images changed the way we build, ship and run our code tremendously.
Some of their benefits include less overhead in system resources, increased portability to different operating systems and more consistent operation as they run the same - regardless of their deployment.
Let's see how we can use the aforementioned advantages to leverage our development environments with one of VS Code's most recent features.

Prerequisites

To follow along you should have VS Code or VS Code Insiders as well as Docker installed and running on your machine.

Additionally the Remote - Containers extension needs to be installed.

Once the aforementioned Remote - Containers is successfully installed, a small green icon should appear in the lower left corner of your editor: Screenshot of the active extension

Taking a look at the examples

As the requirements are now out of the way, let's get into the meat and bones by taking a first look on the official examples.

Clicking on the extension icon opens up the Command Palette where we can choose to "Try a Development Container Sample...": Screenshot of the Command Palette

...which let's us once again choose between several programming languages: Screenshot of the available samples

Selecting Node launches a new instance of your editor with the following content:

.devcontainer
  devcontainer.json
  Dockerfile
.vscode
  launch.json
node_modules
  [...node_modules]
.eslintrc.json
.gitignore
LICENSE
package.json
README.MD
server.js
yarn.lock

Let's inspect the relevant files and build the tension up!

package.json

{
  "name": "docker_web_app",
  "version": "1.0.0",
  "description": "Node.js on Docker",
  "author": "First Last <first.last@example.com>",
  "main": "server.js",
  "private": true,
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.16.1"
  }
}

Looks pretty standard, right? We have express as a dependecy and a start command to spin it up.

server.js

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

const express = require('express');

// Constants
const PORT = 3000;
const HOST = '0.0.0.0';

// App
const app = express();
app.get('/', (req, res) => {
	res.send('Hello remote world!\n');
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);

Here we have a dead simple express server, which will simply return Hello remote world to anyone accessing http://localhost:3000. Easy, let's continue?

.devcontainer/Dockerfile

# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.195.0/containers/javascript-node/.devcontainer/base.Dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 16, 14, 12, 16-bullseye, 14-bullseye, 12-bullseye, 16-buster, 14-buster, 12-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 "umask 0002 && ./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 "npm install -g <your-package-list-here>"

Per default the Dockerfile only contains ARG and FROM to get the base image off of one Microsofts provided ones.
Also not that exciting, huh?

.devcontainer/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.195.0/containers/javascript-node
{
	"name": "Node.js",
	"build": {
		"dockerfile": "Dockerfile",
		// Update 'VARIANT' to pick a Node version: 16, 14, 12.
		// Append -bullseye or -buster to pin to an OS version.
		// Use -bullseye variants on local arm64/Apple Silicon.
		"args": { "VARIANT": "16-bullseye" }
	},

	// Configure tool-specific properties.
	"customizations": {
		// Configure properties specific to VS Code.
		"vscode": {
			// Set *default* container specific settings.json values on container create.
			"settings": {},
			
			// 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": [3000],

	// Use 'portsAttributes' to set default properties for specific forwarded ports. More info: https://code.visualstudio.com/docs/remote/devcontainerjson-reference.
	"portsAttributes": {
		"3000": {
			"label": "Hello Remote World",
			"onAutoForward": "notify"
		}
	},

	// Use 'otherPortsAttributes' to configure any ports that aren't configured using 'portsAttributes'.
	// "otherPortsAttributes": {
	// 		"onAutoForward": "silent"
	// },

	// Use 'postCreateCommand' to run commands after the container is created.
	"postCreateCommand": "yarn install",

	// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
	"remoteUser": "node"
}

This is the most interesting file for us so far!
build.dockerfile contains a link to our aforementioned Dockerfile.
build.args could contain a set of key-value pairs which also contains our used VARIANT.

customizations.vscode is a neat and powerful way to prepare your Dev Container with all our loved and needed extensions.
This example comes with eslint pre-installed, but it could be anything.
This is especially helpful if you have extensions that you only need on specific workspaces and/or would conflict each other.

postCreateCommand let's us run commands such as yarn install to already install all the dependencies upon container creation.

Let's run the example

As we already have everything setup for us, it's just a matter of opening VS Code's integrated terminal in the Dev Container.
You can do so by using the shortcut (⇧⌘P on macOS or Ctrl+Shift+P on Windows and Linux) to open the Command Palette, then typing and selecting "Toggle Terminal" once open.
Running yarn start spins up the small app and exposes the port automatically, which let's us access http://localhost:3000 again, yay!

But how does it work?

Running docker ps or opening Docker Desktop shows the created and running container, which is named vsc-<project-name>-<id>.

The source code is copied, cloned or mounted as a Volume to the Dev Container.
The defined Extensions are installed and run - while also having full access to tools, platform and file system, which enables full IntelliSense, code navigation and debugging.
The ports are either exposed via .devcontainer.json configuration or via auto-forwarding.

Adding a development container to an existing project

Whilst writing this article, I'm taking the chance to create a Dev Container for our HipSquare Blog! Setting it up is incredibly easy, so bear with me:

  • Open up the Remote - Containers Command Palette
  • Choose "Add Development Container Configuration Files..."
  • Choose "Node.js"
  • Choose any version
  • Choose no additional features
  • Hit "ok"

...And that's it!

We now have our new, shiny .devcontainer dir available!
As this blog runs with NextJS and Tailwind, we want to at least have some support for Tailwind, so let's add the extension to the mix:

{
	"customizations": {
		"vscode": {
			"extensions": [
				"dbaeumer.vscode-eslint",
				"bradlc.vscode-tailwindcss" // <-- we only have to add that!
			]
		}
	}
}

Now that's done, let's reopen the Remote - Containers' Command Palette and select "Reopen in Container".
Once VS Code has restarted we're ready to roll in a containerized dev environment, woooh!

Wrap up

We at HipSquare are super excited about the introduction of Dev Container as it's making onboarding and setting up a proper development environment easier than ever.

For further reading please also have a look on the official documentation.
It's super easy to follow along, while also giving a ton of insight on how Dev Container actually work.