Ken Muse

Intro to Dev Container Features


One of the best additions to the dev container specification was the introduction of Features. If you’re not familiar with this functionality, it provides a self-contained way to simplify setup. They make it easier to distribute self-contained packages with container configurations, settings, extensions, and installation code. These can be referenced by the dev container and automatically included in the build. Like dev containers, Features only work for Linux (with most features specifically written to support Debian and/or Ubuntu).

What’s in a Feature?

Behind the scenes, a Feature is just a folder with at least two files:

  • install.sh
    The installation point entry script. This code is run during build time to create a layer in the dev container image. This must be executable within the dev container. This is executed as root during the build.
  • devcontainer-feature.json
    Metadata about the feature, including options or settings that would normally be configured in devcontainer.json. This file can also contain configuration settings which can be set in a devcontainer.json and passed to the install.sh script.

A feature has to be targeted to support one or more Linux flavors. In addition, Features are typically consumed by both ARM64 and AMD64 machines.

How is it used?

Features are referenced in the features section of the devcontainer.json file. They always follow a specific structure:

1"feature-id": {
2    // options, as JSON key-value pairs
3}

The feature-id can be in one of three general formats:

  • A reference to an OCI registry (which covers most of the major container registries). These are in the form registry/namespace/repository:version. For example, ghcr.io/devcontainers/features/go:1 is using the GHCR.io registry, has the tenant namespace devcontainers, has the repository path features/go, and is version 1.
  • An HTTPS URI pointing to a tarball (.tar.gz or .tgz) containing the Feature’s files. For example, https://containers.dev/features.
  • A relative folder reference, such as ./path-to-feature. This allows a feature to be used or developed beside the dev container that will use it.

The community maintains a list of features that have been publicly registered here: https://containers.dev/features

Why not a base image?

You can certainly modify the Dockerfile directly. This can result in a faster build process and an easy way to centralize an image. Features tend to be the right solution for a few problems:

  • The Dockerfile is intended to represent the base of a dev container and a production image. Features are used to configure developer-only additions to the image.
  • The scripts or configuration need to be shared across multiple dev containers which each have a different Dockerfile or base image. Features provide a way to centralize settings and configurations for reuse.
  • A common set of development functionality and configurations are used by multiple teams working with images that have different update lifecycles. Features allow them to layer these features on top of the distributed base images.

It is possible to use a multi-stage Dockerfile and shared/downloaded scripts and packages to achieve a similar behavior. The discussion is similar to software development – do you need packages that support reusability and sharing and do you want to consume community-provided functionality and scripts?

The limitations

The specification is still developing. At the moment, it doesn’t offer support for integrating with the container lifecycle events. This means that you can’t create features that wrapper post-create, post-attach, or other similar events. There is an open issue proposing a change to the specification to include this functionality.

Further details

The full specification is available at https://containers.dev/implementors/features. This is the best place to learn about the functionality and keep up with how it’s changing. Better yet, get involved and help drive the specification!