What Is a Devcontainer? Reproducible Dev Environments
A devcontainer defines a project's dev environment as code — tools, extensions, and dependencies — so every contributor gets an identical setup.
A devcontainer (development container) is a project’s development environment defined as a configuration file — the base image, tools, extensions, and startup commands needed to work on the code — so opening the project in a supporting editor builds and attaches to a container with everything already set up, rather than a contributor manually installing a language runtime, a database client, and a dozen CLI tools by hand.
The problem it solves
“Works on my machine” is usually a dependency problem, not a code problem: one developer has a slightly newer compiler, another is missing a system library, a third has an environment variable set from a project they worked on two years ago. Onboarding a new contributor traditionally means a README full of install steps, each one a chance to drift from what the rest of the team actually has.
Devcontainers move that setup out of a document and into a checked-in configuration file. The environment becomes part of the repository, versioned alongside the code it supports, so a change to the required tool version is a pull request like any other, not a Slack message asking everyone to reinstall something.
What’s inside a devcontainer.json
A devcontainer is configured with a devcontainer.json file, typically at .devcontainer/devcontainer.json, alongside a Dockerfile or a reference to an existing image:
{
"name": "my-app",
"image": "mcr.microsoft.com/devcontainers/typescript-node:20",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"customizations": {
"vscode": {
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
},
"postCreateCommand": "npm install",
"forwardPorts": [3000]
}
This declares a base image, an optional feature (a reusable install script for a common tool), editor extensions to install automatically, a command to run once the container is created, and a port to forward to the host so a locally running dev server is reachable from the browser. An editor that supports the spec reads this file, builds or pulls the image, mounts the project’s source into the container, and opens a fully configured workspace — extensions installed, dependencies fetched, ports wired up — without the developer running any of those steps by hand.
How it differs from a plain Dockerfile
A project already using Docker or Docker Compose for its production build might reasonably ask why a separate devcontainer layer is needed at all. The distinction is intent: a production Dockerfile is optimized to be small, secure, and fast to deploy, and typically excludes anything a developer would want while writing code — a debugger, linters, shell conveniences, editor tooling. A devcontainer is optimized for the opposite: it’s fine to be larger and slower to build if it means the environment is fully equipped for day-to-day development, including tools that would be dead weight in a shipped image.
In practice, many projects build the devcontainer from the same base image as production, adding development-only layers on top, so the two stay close without duplicating the entire dependency list. This mirrors the split some teams already use between a lean runtime image and a heavier build-time one in a multi-stage Docker build — the same base, different tooling loaded on top depending on the job.
Devcontainer vs a local setup vs a shared VM
| Devcontainer | Manual local setup | Shared remote VM | |
|---|---|---|---|
| Consistency across contributors | High — same image for everyone | Low — depends on each machine | High, but shared |
| Onboarding time | Minutes, mostly automated | Hours, following a README | Depends on provisioning |
| Isolation from host machine | Full — runs in a container | None | Depends on VM setup |
| Works offline once built | Yes | Yes | No, needs network access |
| Config lives in the repo | Yes, versioned with the code | No, lives in a doc or wiki | Usually managed separately |
Where the underlying data lives
A devcontainer’s filesystem is ephemeral by default — rebuild the container and anything not explicitly persisted is gone. Source code is typically bind-mounted from the host so edits survive rebuilds, while things like package manager caches or database data usually go in named volumes so they persist across container recreation without living inside the image itself; see Docker volumes vs bind mounts for how that distinction plays out in practice. This same setup also plays well with a git worktree: each worktree can mount its own devcontainer, letting a contributor work on two branches in fully isolated environments simultaneously.
When it’s worth adopting
Devcontainers pay off fastest on projects with a nontrivial or fast-changing toolchain — multiple language runtimes, native dependencies, a specific database version, or CLI tools that are easy to get subtly wrong. For a simple project with a single, stable dependency (a lockfile-pinned Node version, say — see what a lockfile is), the overhead of maintaining a container config may not buy much over a good README. The calculus shifts fast, though, once a team grows past a handful of contributors or the environment includes anything that’s genuinely painful to set up by hand.
The takeaway
A devcontainer turns a project’s development environment into a checked-in configuration file instead of a manual setup process, so every contributor — and CI, if it reuses the same image — gets the identical toolchain without a lengthy onboarding doc. It trades a small amount of upfront configuration work for consistency that scales with the number of people touching the codebase, and it composes naturally with the Docker and version-control workflows most teams already use.
Tagged
Keep reading
The Lycoris Team · · 5 min read Dockerfile CMD vs ENTRYPOINT: Differences and When to Use
CMD sets a container's default command or arguments; ENTRYPOINT sets the fixed executable. How CMD and ENTRYPOINT interact, exec vs shell form, and PID 1.
Chisato · · 5 min read What Are Reproducible Builds? Verifiable Software, Explained
Reproducible builds produce bit-for-bit identical output from the same source, so anyone can verify a binary. Why they matter and how to achieve them.
Chisato · · 5 min read Pulumi vs Terraform: Code vs Declarative Config for IaC
Pulumi defines infrastructure in general-purpose languages like TypeScript and Python; Terraform uses its own declarative HCL. Here's how they differ.