•8 min read

Dev Containers Guide: Docker Environments in VS Code

Dev Containers Guide: Docker Environments in VS Code

I once lost an entire sprint chasing a bug that only appeared on one developer's machine. The culprit? A minor Node.js version mismatch. We spent three days in meetings, code reviews, and frustrated debugging sessions. The fix took five minutes once we found it.

That experience changed how I think about development environments.

The "works on my machine" problem has plagued our industry for decades. We've tried everything: elaborate setup scripts, detailed README files, wiki pages that rot within weeks. Nothing stuck. Until I discovered Dev Containers.

Audio Briefing
0:00 / 0:00

What Exactly Is a Dev Container?

A Dev Container is a Docker-based development environment you configure once and everyone on your team gets the exact same thing. Your code runs inside a container that has all the dependencies, tools, and configurations pre-installed.

Think of it as a virtual machine, but lighter and disposable. You can destroy it, recreate it, and share it with a single configuration file.

The setup lives in a .devcontainer folder at the root of your project. Inside you define your base image, the extensions your team needs, and any post-creation scripts. When someone opens the project in VS Code, they're prompted to reopen in the container. One click, and they're ready.

Does that sound complicated? I thought so too at first.

Advertisement

The Pain I Was Trying to Solve

Before Dev Containers, I worked on a Node.js project where half the team used macOS and half used Linux. Sounds fine, right? Except our native module dependencies compiled differently on each platform. Every few weeks, someone's local environment would drift.

I remember watching a new developer struggle for two days just to get our frontend building. The onboarding doc was eight pages long. It mentioned steps that hadn't been necessary for months. We all forgot to update it whenever something changed.

That's not an uncommon story. The time developers spend fighting their environments is time not writing features.

Setting Up Your First Dev Container

The beauty is in the simplicity. Start with this structure:

your-project/
├── .devcontainer/
│   ├── devcontainer.json
│   └── Dockerfile

The devcontainer.json file is where everything comes together. Here's a basic configuration I started with:

Create the configuration file

Add a .devcontainer/devcontainer.json to your project root. This file tells your editor how to build and connect to your development container.

Specify your base image

Choose a Docker image that matches your stack. For Node.js projects, I typically start with mcr.microsoft.com/devcontainers/javascript-node. The Microsoft repository has images for almost every popular stack.

Install your tools

Use the features property to add common tools without writing custom Dockerfile commands. Need Git, Docker CLI, or database tools? One line each.

Add your extensions

List the VS Code extensions your team needs. These get installed automatically when the container starts. My JavaScript projects always get ESLint, Prettier, and the Docker extension.

Here's what one of my real configurations looks like:

{
  "name": "My Project Development",
  "image": "mcr.microsoft.com/devcontainers/javascript-node",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "extensions": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode"
  ]
}

That's it. Fourteen lines and my entire team works in identical environments.

What Happens When You Open a Dev Container?

The first time you open a project configured this way, your editor (VS Code or Cursor) detects the configuration. It builds the Docker image, creates a container, and connects you to it. You're working inside the container, not on your host machine.

Your terminal is now a bash session inside that container. Your extensions run there. Your file system is mounted from your host, so you edit normally, but execution happens inside the container.

The first build takes a few minutes. After that, starting the container takes seconds.

Advertisement

The Results Surprised Me

I expectedDev Containers to add overhead. I expected them to feel sluggish or complicated. What I found was the opposite.

My team onboarded three new developers in the following months. Each one was writing code within twenty minutes of cloning the repository. No "can you help me install X", no "what version of Y do you have", no mysteries.

We also stopped having environment-related bugs entirely. When everyone runs the same OS, same tools, same versions, those category of problems simply vanish.

The productivity gains weren't dramatic in a visible way. Nothing that would appear in a sprint report. But I noticed it in smaller things: fewer Slack messages about setup issues, more time in flow state, quicker code reviews.

When Dev Containers Make Sense

I've learned through experience where they shine brightest.

Team projects benefit most. When multiple developers touch the same codebase, environment consistency pays compound dividends.

Onboarding situations are obvious wins. Every new hire I've seen use them gets productive faster than those following traditional setup docs.

Complex dependencies become manageable. Projects needing databases, caches, or specialized tools are perfect candidates.

CI/CD parity is a hidden benefit. If your production builds run in containers, dev containers let developers catch the same issues locally.

However, they're not always the answer. Quick personal scripts, single-file experiments, or throwaway exploration don't need this ceremony.

Some Things I Wish I'd Known Earlier

Don't make your base image too custom. I made that mistake once, building an image that took twenty minutes to rebuild whenever dependencies changed. Stick to established base images and add your customizations on top.

Start simple. My first devcontainer.json was overly complicated. I had features and scripts I never used. Add things only when you need them.

The host file system is mounted, but performance can suffer for large node_modules folders. If you hit this, look into caching strategies in the Microsoft documentation.

The Problem That's Actually Solved

We've been tolerating environment inconsistency as an unavoidable cost of collaborative software development. Dev Containers changed that assumption for me.

Three years into using them daily, I can't imagine returning to the old way. Every few months there's a new developer who pushes back, saying it sounds unnecessary. They've all changed their minds within the first week.

The "works on my machine" excuse? It stopped being a phrase in our team lexicon. We don't need elaborate onboarding documents anymore. We don't need "it worked yesterday" debugging sessions.

A standard Docker container runs an application runtime (like a Node server). A Dev Container turns the container into your complete development environment, installing IDE extensions, developer CLI tools, debuggers, and language servers directly inside the container.

The .devcontainer/devcontainer.json file instructs the VS Code Dev Containers extension how to build the Docker image, map host ports, mount project volumes, and configure workspace settings and extensions automatically.

To eliminate bind-mount performance overhead on macOS/Windows, clone repositories directly into a named Docker volume using the Dev Containers: Clone Repository in Named Container Volume command in VS Code.


You Might Also Like

Test Your Knowledge

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Docker Compose for Local Development: A Complete Setup Guide
docker

Docker Compose for Local Development: A Complete Setup Guide

A comprehensive guide to configuring Docker Compose for local development: multi-container architecture, multi-stage Dockerfiles, healthcheck-driven startup ordering, environment secrets management, BuildKit cache mounts, and production-mirrored networking.

Read more