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.