Aller au contenu principal
Nicolas Cousin Tech SolutionsNicolas Cousin Tech Solutions
Module 2 of 5

Adding a Dev Container Feature

Recap: the project from the previous module

This module starts exactly from my-node-api from module 1 — same index.js, same base devcontainer.json. Keep it open.

What Dev Container Features are

A Feature is a reusable, versioned installation script for a tool or runtime, packaged as an OCI image published on a container registry (typically ghcr.io). Instead of writing your own apt-get install commands or maintaining a custom Dockerfile, you reference an existing Feature and it installs automatically during the image build — on top of whatever base image you chose.

The official registry lives at containers.dev/features. That's where you find, for every Feature, its exact reference and available major version — never guess this string, it changes over time and one typo is enough to break the build.

Scenario: adding Python to the Node project

Say your team adds a small Python script to the project (for a data-processing task, a report generator, whatever) without wanting to turn the whole project into a Python image. You keep mcr.microsoft.com/devcontainers/javascript-node:20 as the base image, and add Python on top of it, via a Feature.

Verified against the official registry at the time this module was written: the official Python Feature is referenced as ghcr.io/devcontainers/features/python:1. If you're adding a different tool than the one in this example, always go back to containers.dev/features (or the devcontainers/features GitHub repo) and copy the exact reference and major version shown on that specific Feature's page — don't reuse a reference found elsewhere without re-checking it.

Step 1 — the features field

Edit .devcontainer/devcontainer.json to add the features field:

{
  "name": "My Node API",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",
  "postCreateCommand": "npm install",
  "forwardPorts": [3000],
  "features": {
    "ghcr.io/devcontainers/features/python:1": {}
  },
  "customizations": {
    "vscode": {
      "extensions": ["esbenp.prettier-vscode"]
    }
  }
}

The exact syntax

features is an object, not an array. Each key is a full reference in the form <registry>/<namespace>/<name>:<major-version> — here, ghcr.io/devcontainers/features/python:1. Its value is an options object for that Feature; {} means "use the defaults." Some Features accept options (a specific tool version, for instance) — the Feature's own page on the registry documents which options are available for that Feature specifically, don't assume they're the same across Features.

A common mistake is dropping the major version after the colon, or treating features as an array of strings: both either fail the build or get silently ignored, depending on the exact error.

You can also go through the UI instead of editing JSON directly: the command palette has Dev Containers: Configure Container Features, which opens a picker listing Features from the official registry and writes the exact reference into devcontainer.json for you — handy to avoid any typo, but the resulting JSON object is exactly the same as the one written by hand above.

Step 2 — rebuild (not just reopen)

The container already exists from module 1: simply reopening it isn't enough, since Features install during the image build. From the command palette: Dev Containers: Rebuild Container.

VS Code rebuilds the image taking the new features field into account, which takes a bit longer than the first open (downloading and running the Python install script), then relaunches the window inside the rebuilt container.

Step 3 — the proof

Open an integrated terminal (now running inside the rebuilt container) and check:

python3 --version

Expected output: a Python 3.x version (e.g. Python 3.12.x), even though the javascript-node:20 base image doesn't ship Python natively. If the command answers, the Feature installed correctly on top of the base image without you ever touching a Dockerfile.

Also check nothing regressed on the Node side:

node --version

Still the 20 branch, exactly as in module 1 — a Feature is added on top of the base image, it doesn't replace it.

What you just did

You added a complete runtime to an existing container without writing a single line of Dockerfile, by referencing an official Feature with the exact registry syntax, and then proving its installation with a command, not by trusting it worked. The "devcontainer" thread of this track ends here. In the next module, complete change of subject: you write your first multi-hook pre-commit configuration, on a different project — this new thread doesn't depend on anything you just did.

Check your understanding

What is the correct structure for the `features` field in devcontainer.json?

Where should you verify the exact reference (registry, name, version) of a Dev Container Feature before adding it to a project?

After adding a Feature to devcontainer.json, what's needed for it to actually be installed?

Want to hear about the next modules?

The Academy stays free and open-access, no sign-up required. If you'd just like to be notified by email when a new module ships, here you go — no obligation, unsubscribe anytime with one click.