Photo by Teng Yuhong on Unsplash

Introduction to Dev Containers

An introduction to Dev Containers in Visual Studio Code

Published in
8 min readJun 19, 2024

--

This article provides an introduction to Dev Containers in Visual Studio Code (VS Code) and demonstrates the benefits of using Java development as an example.

In short, a Dev Container allows you to set up a development environment and tooling within a docker container and interact with that container via VS Code, just as you would a workspace hosted on your own laptop. The main benefit is there’s no need to install tooling on your own laptop, and this makes it easy for new starters to start working on a project.

Recently I found myself volunteered to work on a small Java based solution. It’s been some 6 years since I last touched Java, and I must admit I was unsure where to start. A quick search reveals there are now many options regarding choice of Java Development Kit (JDK).

From the start, I had a couple of concerns:

  1. I’d rather not have to organise a license for an IDE
  2. I’d rather not have to install the JDK on my laptop so as to make it easy to change JDK if necessary (at this point, I wasn’t sure what JDK I would be using on the upcoming project)

These concerns led me to use Dev Containers within VS Code.

Prerequisites

A prerequisite is to have docker (CLI) installed. When using a mac, the VS Code documentation recommends Docker Desktop, however it also lists some alternatives. For various reasons I did not have access to Docker Desktop and so ended up using one of the alternatives, namely Rancher Desktop.

The other prerequisites are to have VS Code installed and, within VS Code, install and enable the Dev Containers extension provided by Microsoft.

At time of writing I was using the latest v1.89.1 of VS Code.

Choice of JDK

The maintainers of VS Code do provide some pre-baked containers in GitHub. This includes a container for Java development. However, this container uses a Microsoft JDK, and considering that my upcoming project would involve hosting the solution in AWS, I was keen to use Amazon Corretto.

I found two approaches to use Amazon Corretto:

  1. Use configuration rather than scripting. This is explained in the next section.
  2. Create a Docker image with a Dockerfile that includes the commands to install the JDK. This is explained in an appendix at the end of the article.

Approach 1 — Configuring a Dev Container

First of all, on my laptop, I setup a skeleton java project (named java-batch), with a directory structure as below.

java-batch/
src/
main/
java/
com/
mat/
Batch.java
resources/
logging.properties

Note, considering the simple requirements of the upcoming project, I am deliberately trying to keep the java project as light weight as possible, so just simple Java classes, no spring frameworks etc.

I opened that project in VS Code. At this point, VS Code does not recognise the project as being a Java project (as I don’t yet have any Java extensions installed).

The next step is to add the following file under a “.dev-container” directory.

java-batch/
.dev-container/
dev-container.json

The contents of dev-container.json are shown below.

{
"name": "Java Development",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/git:1": {
"version": "latest",
"ppa": "false"
},
"ghcr.io/devcontainers/features/java:1": {
"version": "21",
"jdkDistro": "amzn",
"installMaven": "true",
"mavenVersion": "3.9.7",
"installGradle": "false"
}
},
"containerEnv": {},
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"vscjava.vscode-java-pack"
]
}
}
}

This configures a container (named “Java Development”) based on an image provided by Microsoft on Docker Hub. I found it necessary to use the “ubuntu” tag, and I suspect this is related to this link.

Under features, two features are included. The first ensures the latest version of git is installed in the container. The second installs the Amazon Corretto JDK v21, as well as Maven v3.9.7. The “java” feature uses sdkman to install the specified JDK, and this is why the jdkDistro is set to “amzn”.

Under extensions, the “vscjava.vscode-java-pack” extension installs the Extension Pack for Java published by Microsoft. This adds useful Java tools to VS Code (within the container), including an interactive debugger.

There are some advanced features of Dev Containers not described in this article. Things like mapping local directories as volumes, which is useful for cacheing dependencies between sessions.

Using the Dev Container

With the Dev Containers extension installed and enabled, VS Code provides a new (green) button at the bottom left of screen. When you hover over this it reads “Open a Remote Window”.

The commands described below are also available from the Command Palette rather than relying upon the green button at bottom left.

With the java-batch workspace already open in VS Code, upon clicking the green button, some container related options are presented. Selecting either the “New Dev Container…” or “Reopen in container” options will open the workspace in a container. This takes a few minutes depending upon how many docker images and layers need to be pulled. You can click the “Show log” option to monitor the progress.

Eventually, VSCode opens, and the Java Projects are listed to the left in the Explorer side menu. Further, if you open a new bash terminal, you can verify the intended JDK is being used, as shown below.

java -version
openjdk version "21.0.3" 2024-04-16 LTS
OpenJDK Runtime Environment Corretto-21.0.3.9.1 (build 21.0.3+9-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.3.9.1 (build 21.0.3+9-LTS, mixed mode, sharing)

Upon entering a container, the bottom left of screen will show a message, something like “Dev Container: Java Development @ rancher-desktop” in my case.

Next, modify the Batch.java file to include a main function (refer to sample below). Then, run this with the debugger and stop it with a breakpoint. At this point, you now have a workspace in a container with the tools needed to perform Java development.

package com.mat;

import java.util.logging.Level;
import java.util.logging.Logger;

public class Batch {
private static final Logger logger = Logger.getLogger(Batch.class.getName());

public static void main(String[] args) {
do {
logger.info("hello world");

try {
Thread.sleep(5000);
} catch (InterruptedException e) {
logger.log(Level.WARNING, "Interrupted!", e);
}
} while (true);
}
}

Exiting the Container

To exit the container, again click the green button at the bottom left of the screen, and select the “Reopen Folder Locally” option.

Any changes made to the java-batch files whilst in the container, for example adding the Maven Wrapper binary or a new “.java” file, are reflected in the local workspace.

When it’s time to do more Java development, you can re-enter the container to use the Java tooling.

Tip: it is possible to open a terminal on the host without exiting the container. In the command palette, search for the “Create New Integrated Terminal (local)“ command.

Conclusion

Dev Containers are an effective way to establish a consistent development environment without the need to manually install any tooling (beyond the prerequisites). Development environments are consistent between developers and can also be consistent with test and production environments.

You have the choice of using pre-baked containers or defining containers of your own. The definitions reside alongside the source code in your source control, making them easy to find and use for any new starters.

This article has focused on Java development, however the same approach can be applied to any other languages and tooling such as dotnet, golang, python, node and more. Quite often, to work on multiple different Node-based applications, it is necessary to switch to particular versions of Node. Node Version Manager (nvm) is a common and effective means, however using Dev Containers would avoid the need for that manual step of ensuring you are using the right node version as listed in the readme (or package.json).

You can find a detailed explanation of Dev Containers here.

Appendix: Approach 2 — Creating a Container

Previously, the article described how to configure a base container to provide the Amazon Corretto v21 JDK. Below are the instructions for creating a custom container. There are detailed instructions to create your own Dev Container that can be found here.

Starting with the same java-batch project described above, the next step is to add the following files under a “.dev-container” directory.

java-batch/
.dev-container/
dev-container.json
Dockerfile

The contents of each are shown below.

dev-container.json

{
"name": "Java Development",
"build": {
"dockerfile": "./Dockerfile",
"context": "."
},
"features": {
"ghcr.io/devcontainers/features/git:1": {
"version": "latest",
"ppa": "false"
}
},
"containerEnv": {
"JAVA_HOME": "/usr/lib/jvm/java-21-amazon-corretto"
},
"customizations": {
"vscode": {
"settings": {},
"extensions": [
"vscjava.vscode-java-pack"
]
}
},
"remoteUser": "root"
}

Under features, the only feature is “git”, which ensures the latest version of git is installed in the container.

Under extensions, the “vscjava.vscode-java-pack” extension installs the Extension Pack for Java published by Microsoft. This adds useful Java tools to VS Code (within the container), including an interactive debugger.

Dockerfile

FROM amazoncorretto:21
USER root
ENV JAVA_HOME /usr/lib/jvm/java-21-amazon-corretto
ENV PATH "${JAVA_HOME}/bin:${PATH}"

The Dockerfile itself is very simple. It is based off of the amazon corretto 21 image published by AWS (on docker hub here). This is a linux based image with the Corretto JDK (v21) already installed. The docker file then ensures that the JAVA_HOME variable is correctly set, and that the JDK’s bin folder resides on the PATH.

Using the Dev Container

With the Dev Containers extension installed and enabled, VS Code provides a new (green) button at the bottom left of screen. When you hover over this it reads “Open a Remote Window”.

With the java-batch workspace already open in VS Code, upon clicking the green button, some container related options are presented. Selecting either the “New Dev Container…” or “Reopen in container” options will open the workspace in a container. This takes about a minute, you can click the “Show log” option to monitor the progress.

Due to the choice of base image, and specifically the operating system of that base image, a prompt is shown to confirm you want to proceed with an older Linux distribution. You can read more about that here. In future, to avoid this prompt, I intend to modify the Dockerfile to build a base image from a supported operating system, and include steps to install the Corretto JDK.

Eventually VSCode opens, and the Java Projects are listed to the left in the Explorer side menu. Further, if you open a new bash terminal, you can verify the intended JDK is being used as shown below.

java -version
openjdk version "21.0.3" 2024-04-16 LTS
OpenJDK Runtime Environment Corretto-21.0.3.9.1 (build 21.0.3+9-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.3.9.1 (build 21.0.3+9-LTS, mixed mode, sharing)

How about Maven?

With this approach, you also need to install Maven. To accomplish this, follow the official steps to install Maven in a terminal in the container and then the steps to add the Maven Wrapper to the java-batch project.

Note, once the wrapper is added to the project, the installed version of Maven (on the path) is no longer necessary, and so there is no need to bake Maven into the container’s image. Maven gets used once, to add the Maven Wrapper, and then it is no longer needed.

At the time of writing, the steps I used looked like the following.

TMP_DIR=`pwd`
cd /root
curl https://dlcdn.apache.org/maven/maven-3/3.9.7/binaries/apache-maven-3.9.7-bin.tar.gz --output maven.gz
tar -x -f maven.gz
export PATH=/root/apache-maven-3.9.7/bin:${PATH}
mvn -version
cd ${TMP_DIR}
mvn wrapper:wrapper
./mvnw -version

--

--