Discover DevContainers: Revolutionize Your Development Workflow with Reproducible Environments!

Mihai Bojin
5 min readApr 9, 2023
Futuristic developer workstation with reproducible environments

Hey there, fellow developer! Have you ever struggled with inconsistent development environments and pesky setup discrepancies?

If so, continue reading!

Meet Dev(elopment) Containers, an open specification for enriching containers with development-specific content and settings.

Why should I care, you say!?

Because it helps you define and run your development environment as code using Docker containers. Plus, it plays super nicely with Visual Studio Code.

In this article, we’ll walk you through the basics of DevContainers and guide you step-by-step to set up your own custom container.

Let’s dive in and make your development experience a whole lot smoother!

Step 1: Understanding DevContainers

DevContainers is an open-source technology that allows developers to define their development environments as code using Docker containers. It integrates seamlessly with Visual Studio Code, providing a consistent environment regardless of your operating system.

This ensures that you won’t get any nasty surprises after a brew upgrade, apt upgrade, moving to a different machine, or upgrading its OS. Use any machine you want, as long as Docker is installed… easy!

When working in a team, it will help you all collaborate effectively and avoid any discrepancies (i.e., the infamous it works on my machine problem).

Start by installing the ms-vscode-remote.remote-containers extension in Visual Studio Code.

Step 2: Setting up Git credentials

Since (I assume) you’ll be working with git repositories, you will need to pull and push code. Of course, you could choose to do that from your host, but why struggle!?

DevContainers supports both HTTP and SSH authentication methods.

For HTTP (i.e., git clone https://…), you’ll need to configure a Git credential manager, which can be a bit more complicated (this article provides all the details.)

However, SSH is more straightforward. The DevContainers extension will forward your SSH agent by default. So, expect something along these lines in your ~/.ssh/config file (or add them if not).

Host *
AddKeysToAgent yes
IgnoreUnknown UseKeychain
UseKeychain yes

You need the Host and AddKeysToAgent directives; the following two lines are used for MacOS/Linux compatibility.

If you sign your commits with GPG, rest assured it will work seamlessly as long as gnupg2 is installed in the container image.

Step 3: Customizing your DevContainer

While the vscode (Visual Studio Code) extension can generate a basic config, the real power comes via customization.

Here are a few things you can change:

  • devcontainers.json: allows you to choose which Dockerfile to build from, make various vscode customizations (inside the container), select extensions to install, and define lifecycle scripts; further details are at https://aka.ms/devcontainer.json
  • Dockerfile: define a Docker image that preinstalls system dependencies (i.e., with a Linux package manager such as apt) and tools
  • lifecycle scripts: lets you specify additional code to run at specific points during the creation of the image (or every time you attach to it)

Where should I store these files?

If you only plan to edit devcontainers.json, then create it in the root of your repo.

However, if you plan to configure a Dockerfile and define lifecycle scripts, then store everything in the “.devcontainer” subdirectory, under the root of your project.

Let’s start by creating a devcontainer.json file with the following content:

// For format details, see <https://aka.ms/devcontainer.json>.
{
"name": "Go",
"build": {
"dockerfile": "Dockerfile",
"args": {
"VARIANT": "1.20-bullseye"
}
},
"customizations": {
"vscode": {
"settings": {
"go.toolsManagement.checkForUpdates": "local",
"go.useLanguageServer": true,
"go.gopath": "/go"
},
// Add IDs of extensions you want installed
// when the container is created.
"extensions": [
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"golang.Go",
"ms-azuretools.vscode-docker",
"ms-vscode.makefile-tools",
"redhat.vscode-yaml"
]
}
},

"remoteUser": "vscode",
"features": {
// install NodeJS
"ghcr.io/devcontainers/features/node:1": {},
// connect to Docker Engine running on the host machine
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
// alternatively you can also run Docker-in-Docker
},
// Run additional initialization commands.
// Also see <https://containers.dev/implementors/json_reference/>
// for a complete reference on lifecycle scripts.
"onCreateCommand": "bash .devcontainer/onCreateCommand.bash"
}

This will create an image with Go 1.20 installed and configure a few vscode extensions and DevContainers features (e.g., above, NodeJS, and forwarding the socket of Docker Engine running on your host). A complete list of supported features can be found at https://containers.dev/features.

Next, let’s create a Dockerfile for Golang 1.20:

bashCopy code
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon)
ARG VARIANT="1.20-bullseye"
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT}

# Install additional packages
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends \
bat \
fd-find \
fzf \
gnupg2 \
jq \
libcrypt-ssleay-perl \
libnet-ssleay-perl \
vim \
;

# [Optional] Uncomment the next lines
# to use go get to install anything else you need
# USER vscode
# RUN go get -x <your-dependency-or-tool>

One thing to note is that any Dockerfiles you define must use one of the base images available at https://github.com/devcontainers/images/tree/main/src.

Finally, let’s install additional components via a bash script that runs once after the container has been created.

Populate an onCreateCommand.bash script in the “.devcontainer” directory with the contents below.

It will install asdf-vm and then rely on asdf plugins to install other tools.

This is a good alternative for programs that are either not available via the underlying Linux package manager, or that you may want to fix to specific versions. Another use-case I personally use this lifecycle for is to define common aliases I rely on during my usual workflows.

# helper function that persists settings for bash/zsh interactive sessions
persist() {
echo "$1" | sudo tee -a /etc/bash.bashrc
echo "$1" | sudo tee -a /etc/zsh/zshrc
}

# install asdf
git clone <https://github.com/asdf-vm/asdf.git> ~/.asdf
persist ". ~/.asdf/asdf.sh"
source ~/.asdf/asdf.sh

# define plugins
asdf plugin add kubectl <https://github.com/asdf-community/asdf-kubectl.git>
# define other plugins...

# install everything
asdf install

# install go tools (e.g., delve)
go install -v github.com/go-delve/delve/cmd/dlv@latest

# define aliases
persist "alias k=kubectl"
persist "alias gst='git status'"
# and more ...

Step 4: Opening your DevContainer in Visual Studio Code

With your custom DevContainer defined, it’s time to open it in Visual Studio Code. Ensure that you have the Remote — Containers extension installed.

  1. Open Visual Studio Code.
  2. Click on the Remote Explorer icon in the Activity Bar on the side of the window.
  3. In the Remote Explorer, click the “Open Folder in Container” button.
  4. Browse to the directory containing your custom DevContainer and select it.

Visual Studio Code will now build and run your custom DevContainer, providing a consistent and reproducible development environment.

DevContainer is an excellent solution for streamlining your development workflow and creating reproducible environments.

With this step-by-step guide, you’re ready to create your custom DevContainer. So try it and enjoy the benefits of a unified development experience!

--

--

Mihai Bojin

Software Engineer at heart, Manager by day, Indie Hacker at night. Writing about DevOps, Software engineering, and Cloud computing. Opinions my own.