How to tutorial
Development Containers and CLI Tools — a Match Made in Heaven
Part 1: How to build a development container with VS Code and Docker (plus a demo video)
Dev containers and CLI tools have been gaining popularity among open-source and commercial projects. Recently, I built a dev container for Polaris SLO Cloud, an open-source project I am involved with. Previously, I reported on main lessons learned. In this two-part series, I will give a hands-on tutorial on how to build a development container, with VS Code and Docker (Part 1 — this article) and how to create a CLI tool, with Nx (Part 2).
Building a dev container with Docker and VS Code
In this section, I will show how to build a dev container from scratch using VS Code and Docker. Later in the article, I show a demo video on how to use Visual Studio Code Remote — Containers extension to run the dev container. To be able to follow along and run the demo you will first need to install:
- Docker Desktop for Windows/Mac or Linux
- Visual Studio Code and Remote Development extension pack
- Minikube (Optional; Only needed to run the demo)
Development environment, source code, and dev containers
At the end of this section, we will be able to connect an IDE (in this case VS Code) running on a host machine, e.g., a laptop, to our development container. This container will be running a fully configured development environment. Finally, we will be able to fetch the latest version of the source code from our Git repo and start developing inside the IDE running on our host machine.
The source code can be cloned on the local file system or it can be cloned on a persistent docker volume. In our demo below, we show how to do the latter, as mounting from the local file system is currently not recommended due to performance issues. It is interesting to note that there are no dependencies between the container and the source code. This can be beneficial for sharing the dev containers or in terms of offering additional flexibility to developers.
A fully configured development environment should contain all the required development/build tools such as language SDK and Git, runtime services such as a development database, complete configuration models such as networking and environment variables, but also fully configured IDE, with all the required extensions, code styles and so forth. Now let’s see how to build a dev container.
Two main parts are comprising a dev container: a Dockerfile and a devcontainer.json file. In practice, there will often be a third part — a set of shell scripts automating tasks such as config files management and processing. To make a repository “dev containers ready”, one needs to put all these files in a .devcontainer directory in the repository’s root directory. There are some issues with this approach when the repository is a monorepo, but that is out of the scope of this article.
Building dev containers from a Dockerfile
The Dockerfile is just a regular Dockerfile, which is used to build a container image for a dev container. A nice consequence of working with the VS Code is that Microsoft provides many useful container base images.
In our Dockerfile, we use typescript-node as our base image, which pulls in a lot of useful tools, such as, well: TypeScript, Node.js, Git, and so forth. We add kubectl to the container, as it is necessary to run our custom Kubernetes operators. It is possible to run the full stack locally, including also the Kubernetes cluster. Currently, we only support local clusters on Minikube, which needs to be installed and running on the host machine. Finally, using the Dockerfile, we also add a custom bash script to our container. The script is used to mount the host’s .kubeconfig into the container, fix up networking and configure Kubernetes cluster certificates. To keep the host and the container in sync, the script is executed on each login, e.g., when the dev container is started from an IDE.
Configuring dev containers with a devcontainer.json
The second key ingredient of a dev container is a devcontainer.json file, which is VS Code specific. It is basically a config file that tells the VS Code how to built and start a dev container. It also offers some additional configuration of the development environment. Here is a snippet from our devcontainer.json config.
We can see that devcontainer.json enables configuring several interesting things such as:
- Declaring environment variables for the dev container, by using remoteEnv object, which is a little cleaner than using Dockerfile. In our example, we show one simple env var SYNC_LOCALHOST_KUBECONFIG that is used by our copy-kube-config bash script.
- Mounting directories form the host file system into the container file system, by simply declaring a list of mounts. Here we show how to mount a local .kube directory, which contains all the configuration files for the local (host) Kubernetes cluster. This enables our Kubernetes operators, which run inside our dev container, to interact with the local cluster out-of-the-box, without any additional configuration.
- Installing desired VS Code extensions inside the container after it has been created. This enables uniform developer experience and can be used to enforce some company/organization-wide policies. For example, code formatters and linters.
- Forwarding ports (forwardPorts) inside the container so that they are available locally. VS Code will show the local address, e.g., localhost:1234 under the ports tab after the container starts. This feature is very useful in many situations, such as accessing a service running in a container or viewing a WebUI in a browser. I find this more convenient than using native Docker support that requires, e.g., knowing the difference between exposing and publishing ports in Docker.
- Exposing different lifecycle hooks that allow running additional commands in the container. The most useful hook is postCreateCommand — which enables installing additional language, framework, or system packages/dependencies. It fires after the source code has been mounted.
A full list of devcontainer.json options is available here.
The 2 minutes demo below shows our dev container in action — from git clone to running a custom controller on a local Kubernetes cluster in just a few minutes.
In this article, we have seen how to build a dev container. For this tutorial, I used Docker and VS Code to build the dev container, but some other frameworks and tools can be used for this purpose. For example, an interesting alternative to use could be CNCF’s CNAB, which comes with a stand-alone CLI tool.
In general, if you have some experience with containers, starting with dev containers is nothing exotic. As we have seen, it boils down to declaring a Dockerfile (there are also many great prebaked container images out there), configuring a devcontainer.json, and potentially adding some custom scripts, e.g., to configure networking if necessary. That’s it! Now anyone can start contributing to our project in a matter of minutes.
In Part 2, I show how to build a CLI tool and discuss why and how to combine it with development containers.
You can read more about benefits and challenges of development containers in my previous article: “Why Every Open Source Project Needs a Development Container?”.
Why Every Open Source Project Needs a Development Container?
And why you should probably build one too …
If you would like to find out more about our Polaris SLO Cloud project, check out our GitHub or follow me for updates.