Containers are a key component of the modern application platform, providing isolation between applications and, at the same time, turning userland into a portable runtime environment. Containers give us a place to both package and run all the necessary dependencies for our code while managing resources and scaling applications and services up and down.

Outside of tools like GitHub’s Codespaces, there has been very little focus on using containers as a development tool. That’s odd, because containers are an ideal way to package an entire development environment, with all the services needed to build and test your code. Usefully, the tools used to manage Codespaces’ containers are available to anyone, allowing you to build and manage your own dev containers inside your own environment, on-premises or in the public cloud.

Share development environments as containers

The idea behind dev containers is simple: Package all the services needed to support development of an application in a single Docker container. You can then run it in a virtual machine, using remote development and debugging tools to manage your code. The container can host everything from databases to mock API endpoints, as well as necessary runtimes and libraries for your project. Container isolation reduces the risk of interference with other applications.

With dev containers, there’s no need for developers to spend time configuring their machines. All they need to do is download and mount a project dev container and start coding. It’s a big time-saver and allows lead developers and architects to ensure that everyone working on a project is starting from the same page. And if anything goes wrong, throw away your current environment, download a new dev container, clone your git repository, and simply start again where you left off.

Dev containers are starting to be used more and more across Microsoft. The Azure SQL team recently announced a set of predefined, preconfigured templates that are ready to use as part of your own dev containers. These templates provide support for common development tools, with a ready-to-run database. Although including data in a container is normally bad practice, here we’re using them as a development environment, not as a stateless userspace runtime.

If you’re using Windows for application development, dev containers take advantage of the free Visual Studio Code programmer’s editor and the built-in Windows Subsystem for Linux (WSL) virtual machine—everything you need to build and run dev containers for free.

Building a dev container

The dev container specifications are a Microsoft-sponsored open source project, with several GitHub repositories, including tools to go from a description to a running container. The intent is to deliver everything to define not only the applications that you want in your container, but also any necessary settings and configurations. This will allow you to use a dev container to deliver both guidelines and guardrails, as well as coding standards.

It all starts with a description. The initial specification describes a structured JSON format, devcontainer.json, which stores the base configuration of a dev container. Additional information is stored in image labels and in specific scripts called Dev Container Features. The result should describe all the tools to support much of the application development life cycle, taking your code all the way to your CI/CD environment.

Dev container setup needs to be repeatable so that every time you use a description to build a container you get the same result. Everyone building code in a container can get the same output when testing the same branch, simplifying merges and pull requests.

It’s possible to build a container definition from scratch, but it’s easier to use the Visual Studio Code Dev Containers extension (part of the Remote Development Extension Pack) to build and manage containers. Things are even easier if you start with an existing development environment image from Microsoft’s dev containers repository and then add your own customizations.

You can add Visual Studio Code extensions, so a Python application may include a selection of VS Code Python extensions. Other options let you forward ports, allowing you to test application outputs on your host PC and use debugging tools such as Edge’s F12 developer tools.

Dev Containers Features is a handy way to add predefined applications and tools to a container. These reusable snippets of code can be added to an existing dev container definition to add the tool described by the code to your container. They’re distributed as a folder, containing a devcontainer-feature.json definition, plus an install script and any necessary files.

Using dev containers

Once you’re using dev containers as part of your standard toolchain, you can create a library of features that can be quickly added to your container definitions. You can customize off-the-shelf containers or quickly build a new definition for a new project, treating features as building blocks that sit on top of a standard base container that’s been defined for a specific stack.

The basic process of building a dev container makes a lot of sense. It’s a top-down approach, which needs to start with architects and dev leads agreeing on a project stack. You can then find a base platform image, say .Net, in the VS Code container gallery. Once you have that, you customize it for your project, adding new tools by editing the devcontainer.json in VS Code and by adding predefined features. Once the container is ready to use, deploy it and the necessary VS Code tools to your development team.

Your local container host needs to be running Docker or at least have a Docker-compliant CLI on top of its engine. The CLI is key here, as the dev container works through it rather than needing direct access to your container host. That’s both a benefit and a drawback: There’s no dependency on APIs or even on Docker itself. As long as a container environment supports the Docker CLI, you can use it with your dev containers. However, if it doesn’t, you can’t. That means there are issues with alternative container engines, such as Podman, which only support a subset of the Docker CLI. Of course, as dev containers is an open source project, there’s ongoing work to support other container engines, and you can make requests or submit code via GitHub.

Once you have a container running on a local Docker instance (Hyper-V or in WSL), you can use VS Code’s remote tools to first connect to your container and then install a VS Code environment. This will use your local VS Code for UI, installing the extensions you’ve described in your devcontainer.json in the remote instance. This keeps your personal choice of extensions in your local development environment separate from those in your dev container, though of course there is nothing to stop you from adding extensions above and beyond those provided.

These are the same remote development tools you use to work from Windows to WSL or to a remote machine. It’s a proven technology that works well, with support for most common Linux distributions on both Intel and Arm.

By using features to add tools to a container that contains your runtime stack, you now have a new way to test code as part of your CI/CD process. You can make your dev container part of a GitHub Action or an Azure DevOps pipeline, injecting code and running tests in a known environment. Similarly, you could use a runtime container as a host for shipping code, hosting it in a production Kubernetes environment.

Starting development in a container keeps code in the same environment throughout its life cycle, helping developers understand the constraints and advantages that come with cloud-native applications. Dev containers can help teams focus on a consistent foundation for a project, letting everyone build their toolchains in a consistent environment that can be quickly reset and reloaded.