Hermes Wiki
Developer/DeveloperTools/LocalDevEnv/Fundamentals/dev-containers-and-reproducible-local-environments

Dev Containers and Reproducible Local Environments

Concept

A dev container defines a project's entire local development environment — runtime version, system packages, tool chains, editor extensions, even shell configuration — as a versioned artifact checked into the repository, instead of as a wiki page of setup steps a new contributor follows by hand. The Development Containers spec (devcontainer.json, driven initially by Microsoft/VS Code, now an open specification with a growing tool ecosystem) describes how to build or point to a container image and layer environment-specific configuration on top of it: which base image or Dockerfile to use, which ports to forward, which extensions to auto-install, which post-create commands to run (installing dependencies, seeding a database). A supporting tool — VS Code's Dev Containers extension, JetBrains Gateway, or a CLI like devcontainer — reads that file, builds or pulls the container, and connects the editor's UI to a process running fully inside it: file system, language servers, debuggers, and terminal all execute in the container, while only the editor's UI stays on the host.

This is a different problem than a production container. A production Dockerfile packages the application to run; a dev container packages the environment needed to develop the application — a superset that includes debuggers, linters, test runners, and dev-only tooling that would bloat or leak into a production image. The two are related (a dev container often builds from the same base image as production, to keep dev/prod parity) but serve different audiences and get evaluated by different criteria: production images are judged on size and attack surface, dev containers on iteration speed and completeness of tooling.

Tradeoffs

Approach Onboarding time Environment drift Cost
Written setup docs (README steps) Slow — a new contributor manually runs each step, and something is usually stale High — docs silently rot as dependencies change; "works on my machine" is common None beyond authoring/maintaining the doc
Local install scripts (setup.sh) Faster than docs, but still runs directly on the host OS Medium — script drift is easier to catch than doc drift (it either runs or fails), but host OS differences (macOS vs Linux vs WSL) still leak through Low — no container runtime required
Dev container (devcontainer.json) Fast — one "reopen in container" action, environment matches spec exactly Low — the container image is the single source of truth; a rebuild always reproduces the same environment Requires a container runtime (Docker Desktop or equivalent) on every contributor's machine, plus image build/pull time and disk
Cloud dev environment (pre-provisioned remote container) Fastest — zero local setup, just a browser/thin client Lowest — environment is centrally managed, no local variance at all Highest — ongoing compute cost per active environment, plus network-dependent editing latency

The dev-container row is the common middle ground: it gets the reproducibility of a fully specified environment without the recurring compute cost of provisioning a remote machine per contributor, at the cost of requiring a container runtime locally.

When to use / when not to

  • Use a dev container once "works on my machine" has actually happened more than once, or a new contributor's first day is measurably slow because of environment setup — that repeated cost is what justifies the one-time authoring effort.
  • Especially valuable for polyglot projects (a Python backend, a Node frontend, a Postgres dependency) where "install docs" would otherwise mean juggling multiple language version managers by hand; the container collapses all of that into one pinned image.
  • Also valuable for exactly reproducing a CI environment locally — since the same devcontainer.json/Dockerfile can be built by CI, a bug that "only happens in CI" is easier to reproduce by opening the same container locally.
  • Skip it for a small, single-language project where the host's own package manager already installs the one runtime needed reliably — the container's build/pull overhead isn't worth it if setup was already a single pip install -r requirements.txt away from working.
  • Don't reach for a dev container as a substitute for actually documenting why each setup step exists — the container encodes what to run, but a contributor debugging a container build failure still benefits from knowing why a given package or version pin is there.

Common pitfall

Treating the dev container as a one-time setup artifact instead of a maintained one. A devcontainer.json that pins exact versions is exactly as prone to going stale as a setup doc if nobody updates it when a new dependency is added outside the container — the difference is that staleness in a dev container fails loudly (the container simply doesn't have the new dependency, so the build or run step errors out) rather than silently, which is a real improvement, but only if someone actually acts on that failure instead of routing around it by installing the missing thing on the host and forgetting to update the container definition. A dev container that a team quietly stops rebuilding from, and instead uses only as an initial scaffold before diverging by hand, has lost the reproducibility guarantee it exists to provide.

Engineering Lens

The value of a dev container isn't "it's containerized" — plenty of teams get equivalent reproducibility from a well-maintained install script plus a version-pinning file (e.g. a lockfile plus an .nvmrc/.python-version). The real distinction is whether the environment definition is a single artifact that fails fast and visibly when it's wrong, checked in next to the code it serves, and exercised often enough (ideally the same path CI uses) that drift gets caught immediately rather than discovered by the next new hire. A team that adopts dev containers but still has tribal-knowledge setup steps living outside devcontainer.json has only partially solved the problem it was meant to solve.

Sources

Hermes Wiki