Skip to content

The publication for web craftspeople Tuesday, 3 November 2026

Tools

DevContainers: Standardizing a Web Team’s Development Environment

The Development Containers specification lets a team describe a project's entire development environment in a versioned file, so every member opens the same environment in one click. This guide covers its practical setup and its limits.

“Works on my machine” remains one of the costliest phrases on a web development team:
different Node or PHP versions across workstations, a missing editor extension, a forgotten
environment variable. The Development Containers specification, driven in large part by
Microsoft and now implemented by several tool vendors, addresses this by describing the full
development environment — runtime, tools, extensions, ports — in a file versioned alongside
the code.

The problem DevContainers solve

A web project usually depends on a precise list of requirements: a runtime version (Node,
PHP, Python), command-line tools, sometimes a local database, shared editor extensions for
linting or formatting. Documenting that list in a README works for a while, until
the document drifts from reality. The
command-line tools specific to each project
end up varying from one machine to another without anyone noticing until a hard-to-reproduce
bug shows up.

A DevContainer moves that documentation from prose someone has to read to a configuration
the editor actually runs: it builds a container, installs the planned tools inside it, opens
the project folder there, and exposes the required ports. A new team member no longer installs
anything on the host machine beyond Docker and their editor.

Anatomy of a devcontainer.json

The configuration lives in .devcontainer/devcontainer.json, at the repository
root. An example for a Node.js project with PostgreSQL:

{
  "name": "node-web-app",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
  "features": {
    "ghcr.io/devcontainers/features/node:1": { "version": "22" }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ],
      "settings": {
        "editor.formatOnSave": true
      }
    }
  },
  "forwardPorts": [3000, 5432],
  "postCreateCommand": "npm install"
}

“Features” are reusable building blocks that install a runtime or tool (Node, PHP,
Docker-in-Docker, GitHub CLI) without hand-writing a Dockerfile. The
customizations.vscode block automatically installs the team’s extensions and
settings, so linting and formatting are identical for everyone from the moment the project
opens.

Which tools support the specification

ToolSupportNote
Visual Studio CodeNative (Dev Containers extension)The specification’s original reference implementation
GitHub CodespacesNativeReads devcontainer.json directly to provision the cloud instance
JetBrains (IntelliJ, PhpStorm, WebStorm)Native since 2024Same files, no conversion needed
devcontainer CLINativeBuilds and runs the container outside the editor, useful in CI
Docker Compose aloneCompatible as a complementA DevContainer can build on an existing docker-compose.yml

Setting up a DevContainer on an existing project

On a project already running with Docker Compose, the migration comes down to three steps:
add a .devcontainer folder, point it at the existing compose file instead of
writing a new one, then declare the ports and the post-create command. For a team juggling
several Git worktrees in parallel,
each worktree can open its own isolated DevContainer, with its own ports and its own database
state, without interfering with the other working copies.

Secrets should never be written into devcontainer.json, which is versioned with
the rest of the repository. They’re passed to the container through the host’s environment
variables (a remoteEnv or containerEnv field pointing to an
unversioned variable), following the same logic as
handling environment variables cleanly in web development.

A development environment documented in a text file drifts over time; one described in an
executed configuration cannot diverge from the project that defines it.

Limits and pitfalls to know about

On macOS and Windows, disk I/O performance for a Docker volume mounted from the host stays
noticeably slower than on a native Linux filesystem. For a Node.js project with a large
node_modules folder, preferring a named Docker volume over a direct host folder
mount clearly reduces the impact on build times and hot-reload speed.

Base images loaded with preinstalled tools (several gigabytes) slow down the first project
open and the image refresh in CI. It’s better to start from a minimal image and only add the
features actually used. Finally, a DevContainer standardizes the development environment, not
the production one: running the same images in CI keeps tests consistent, but deployment
remains a separate concern, with its own image size and security constraints.

ApproachScopeReproducibility
README with manual instructionsDocumentation onlyLow: drifts over time
Docker Compose aloneServices and dependenciesGood for services, silent on the editor and its extensions
VagrantFull virtual machineGood, but heavier to provision than a container
DevContainerRuntime, tools, editor, portsHigh: a single versioned file drives the editor

Using the same DevContainer in continuous integration

The specification also ships a command-line interface, devcontainer, which
builds and runs the same container outside any editor. A CI pipeline can then run tests in the
exact environment the team uses locally, without maintaining a second image definition in
parallel:

npm install -g @devcontainers/cli

devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . -- npm test

This approach removes an entire class of regressions caused by drift between the development
environment and the CI one: a system dependency installed by hand on a developer’s machine,
then forgotten in the image the pipeline uses, stops being a problem once both environments
share the same definition. The payoff is clear on a PHP project depending on specific native
extensions, or a Node.js project bundling compiled modules: the underlying system library
version no longer varies depending on which machine runs the build.

The trade-off is a slightly longer pipeline startup time compared to a CI image stripped
down to the bare minimum for running tests, since the container also carries the tools and
extensions meant for the editor. For projects where that cost weighs on total pipeline
duration, a CI image derived from the same base but without the editor-specific layers remains
a reasonable option, as long as it’s regenerated automatically on every change to the source
Dockerfile rather than maintained by hand.

Key takeaways

The Development Containers specification moves development-environment configuration away
from documentation someone has to maintain and into a file the editor actually runs. It’s now
natively supported by VS Code, JetBrains IDEs and GitHub Codespaces, which makes it a realistic
standard rather than a proprietary solution. Its main limitation remains disk I/O performance on
macOS and Windows, best addressed with named volumes rather than direct host folder mounts.

On projects he takes over for clients, the author sees the same scenario play out almost
every time: an install README dated several months back, wrong on at least one point by now. A
DevContainer doesn’t fix everything, but it shifts the burden of documentation onto a
configuration the editor checks every time the project opens — which, in practice, saves a
fair amount of first-day debugging back and forth on a new engagement —
Simon Janvier.

Further reading: the full specification at
containers.dev.

Read next