Matt Bolitho


Configurable devcontainers with Docker targets

🎊 Happy New Year! 🎊

A bit late to the party, but this is my first post of 2024 so if you’re reading this: I hope you have a great year!

This weekend, I found out about the build.target property in devcontainer.json files and it filled in a massive knowledge gap for me: adding conditional logic in your devcontainer image.

This could be useful when:

I became motivated to do this when I started adding library dependencies built from source to an experimental repository. I didn’t want to require having those dependencies in the Docker image when I worked on other parts of the code.

This brief post contains a minimal (and somewhat contrived) example of this feature. It assumes you have some familiarity with devcontainers.

Let’s start with a super basic devcontainer, assuming the following file structure:

1/some_repository
2  /.devcontainer
3    devcontainer.json
4    Dockerfile

Initial contents of devcontainer.json:

1{
2  "name": "targets-example",
3  "build": {
4    "dockerfile": "Dockerfile"
5  }
6}

Initial contents of Dockerfile:

1FROM ubuntu:22.04
2
3RUN apt update && \
4    apt install --no-install-recommends -y \
5        gcc \
6        g++ \
7        gdb \
8        git

If you open this as a devcontainer in VS Code, you get a simple Ubuntu + GCC development environment. For the sake of brevity, let’s assume this is sufficient for our hypothetical project. We also don’t mind whatever versions of tools we get from apt. This devcontainer image (on my machine at the time of writing) is 350MB, which isn’t too big for a development environment.

Now, let’s imagine we want to support more compilers! If we add just clang to the list above we’re up to 729 MB. That’s still not too bad though, and it’s likely we’ll be adding new tools of a similar size from time-to-time anyway.

However, if we decide to start using the Intel compilers (as per their apt documentation), we’re up to a whopping 15.6 GB! Ok, there are some tricks to slim that down a little… but it will still get much bigger and we’ll be well into the GBs.

If we have a considerable group of developers or consumers that really want to use Intel toolkits, it would be much nicer to give them the option to do so as a first class feature.

This is where the build.target property in devcontainer.json comes in clutch. We can:

  1. Make our gcc/clang base a Docker target.
  2. Set this to be the default value of the build.target property in devcontainer.json.
  3. Extract our Intel toolchain installation logic to a new Docker target.

To do this, we update our devcontainer files like so:

devcontainer.json:

1{
2  "name": "targets-example",
3  "build": {
4    "dockerfile": "Dockerfile",
5    "target": "base"
6  }
7}

Dockerfile:

 1FROM ubuntu:22.04 as base
 2RUN apt update && \
 3    apt install --no-install-recommends -y \
 4        gcc \
 5        g++ \
 6        git \
 7        wget \
 8        gpg \
 9        ca-certificates
10
11FROM base as intel
12RUN wget -O- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB \
13        | gpg --dearmor | tee /usr/share/keyrings/oneapi-archive-keyring.gpg > /dev/null && \
14    echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" \
15        | tee /etc/apt/sources.list.d/oneAPI.list && \
16    apt update && \
17    apt install --no-install-recommends -y \
18        intel-basekit \
19        intel-hpckit

Our Intel lovers can now modify the value of the build.target in devcontainer.json to intel to opt into using those development tools. They will have to wait for the image to rebuild (and presumably go an make a lot of coffee…) but we will have given them first class support for their toolchain without forcing everyone else to endure the increases in container size and build time.

It should go without saying that this is only one concrete example of when using this feature might make sense. Some other examples are:

However, for the sake of completeness, not all of these problems above need to be solved with tooling. For example, one could contribute a hard to build dependency to a package manager or split code into multiple repositories. Both of these decisions depend on your organisation.

I hope you found this quick post useful!

#Docker #Devcontainer #Environment-Setup