Using VS Code Dev Containers: Building Isolated and Consistent Docker Development Environments

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

The Real Problem: “It works on my machine!”

If you’re a developer, you’ve probably heard or uttered this phrase at least once: “It works on my machine!” This isn’t just a funny anecdote; it’s a nightmare for many development teams. Can you picture this scenario:

  • Starting a new project, you have to install dozens of tools, libraries, and runtimes (Node.js, Python, Java, Go, various databases…).
  • After installation, you realize Project A needs Node.js 16, while Project B requires Node.js 18. Installing them side-by-side easily leads to conflicts; even using nvm/pyenv just adds another layer of complex management.
  • A new team member joins. They spend an entire day, or even several days, just installing and configuring an environment identical to yours, only to still encounter minor errors.
  • You work across multiple operating systems (Windows, macOS, Linux). A configuration that runs perfectly on your Mac might unexpectedly fail when deployed to a Linux server or used by a colleague on Windows.

These situations not only waste time but also cause frustration, reduce productivity, and slow down project progress.

Root Cause Analysis: Why the Discrepancy?

The “it works on my machine” problem often stems from inconsistent and difficult-to-reproduce development environments. Specifically:

  • Differences in operating systems and installed tools: Each operating system (Windows, macOS, Linux) manages packages, paths, and system APIs differently. Even Linux distributions (Ubuntu, Fedora, Arch) exhibit significant variations.
  • Dependency Hell: Working with multiple projects, each potentially requiring different library or runtime versions. Installing everything on the local machine easily leads to conflicts, causing one project to run while another fails.
  • Error-prone manual setup: Manually installing and configuring development environments is prone to errors. This process often relies on memory or outdated documentation. Even a small missed step can cause failures.
  • Lack of a standardized environment: There’s no “golden standard” that ensures every machine on the team has an identical development environment.

Traditional Solutions (and their Limitations)

Historically, we’ve tried various approaches to solve this problem, but each has its own limitations:

Direct Installation on Local Machine

  • Pros: Simplest for a single project, maximizes machine performance.
  • Cons: Prone to “dependency hell” when working on multiple projects. Inconsistent environments across machines. Onboarding new team members is time-consuming due to fresh installations. This is the root cause of the “it works on my machine!” problem.

Using Virtual Machines (VMs)

  • Pros: Good isolation, reproducible environments (by cloning VM images).
  • Cons: VMs are heavy, consume significant resources (RAM, CPU, disk space), and have long startup times. Configuration sharing is inflexible, and integration with IDEs like VS Code is not seamless.

Using Docker (for application runtime only)

Docker was a significant leap forward. It helps package applications and their dependencies into a lightweight, isolated container, ensuring consistent application execution everywhere. itfromzero.com has published numerous guides on Docker for beginners, Docker Compose for multi-container management, and application deployment. However, when using Docker in the traditional way to run applications, your development environment still resides outside the container.

  • Pros: Lightweight, isolated, ensures consistent application runtime environment.
  • Cons: You still have to install development tools (VS Code, compilers, linters, debuggers, Git…) directly on the host machine. Your development environment is not entirely encapsulated within the container. This can still lead to discrepancies among developers’ machines if they have different tool versions on their host.

The Best Approach: VS Code Dev Containers – Isolated and Consistent Development Environments

Dev Containers are the solution. They overcome the limitations of the aforementioned methods by bringing your entire development environment inside a Docker container.

What are Dev Containers?

VS Code Dev Containers (formerly known as Remote – Containers) is a feature of Visual Studio Code. It allows you to open a project folder or a Git repository directly inside a Docker container. Crucially, this container doesn’t just run your application; it becomes your entire development environment.

All project tools, runtimes, libraries, dependencies, and even VS Code extensions are installed and configured inside the container. Your local machine only needs VS Code and Docker.

Key Benefits:

  • Consistency: Every team member works in the exact same environment.
  • Lightning-fast onboarding: New team members simply need to clone the repo, open it in VS Code, and select “Reopen in Container.” Everything will be automatically installed and configured.
  • Project isolation: Each project has its own separate environment, eliminating dependency conflicts.
  • OS independence: Whether you use Windows, macOS, or Linux, your development environment remains consistent.

How Dev Containers Work (Basic Overview)

When you open a project using Dev Containers, VS Code performs the following steps:

  1. Reads the .devcontainer/devcontainer.json configuration file in the project folder.
  2. Uses Docker to build (if necessary) and initialize the container based on the configuration.
  3. Mounts the project folder from the local machine into the container.
  4. Installs VS Code extensions (defined in devcontainer.json) into the container.
  5. Connects VS Code (running on the local machine) to the container. From this point, the terminal, debugger, linter, etc., all run inside the container.

Getting Started with Dev Containers (Step-by-Step Guide)

Step 1: Prepare Your Tools

First, ensure your machine has the following tools installed:

  • Visual Studio Code
  • Docker Desktop (or Docker Engine if you’re on Linux)
  • The “Dev Containers” extension for VS Code. You can search for and install it from the Extensions view (Ctrl+Shift+X) in VS Code.

VS Code Dev Containers Extension

Step 2: Create a Sample Project (e.g., Node.js)

To illustrate, let’s create a simple Node.js project. Open your terminal and create the directory as follows:

mkdir my-node-app
cd my-node-app
npm init -y
echo "console.log('Hello from Dev Container!');" > app.js

Then, open the my-node-app folder in VS Code (code .).

Step 3: Add Dev Container Configuration

In VS Code, open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P on macOS) and type “Dev Containers: Add Dev Container Configuration Files…”.

VS Code will ask you which environment type you want to configure. Select “Node.js” (or your desired language). You can choose the Node.js version and additional features.

After selection, VS Code will create a .devcontainer folder in your project with two main files:

  • devcontainer.json: The main configuration file for the Dev Container.
  • Dockerfile: Defines the Docker image for your environment (if you choose to customize).

For example, the content of the .devcontainer/devcontainer.json file might look like this:

{
  "name": "Node.js Development",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:18",
  "features": {
    "ghcr.io/devcontainers/features/common-utils:2": {
      "installZsh": "true",
      "userName": "vscode"
    }
  },
  "forwardPorts": [3000],
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ]
    }
  },
  "postCreateCommand": "npm install"
}

Explanation of important fields:

  • name: The display name for the environment.
  • image: The base Docker image to be used. Here, it’s Node.js version 18.
  • features: Additional features you want to install into the container (e.g., zsh, nvm, Docker-in-Docker…).
  • forwardPorts: Ports from the container that will be forwarded to your local machine.
  • customizations.vscode.extensions: A list of VS Code extensions that will be automatically installed in this container. This is very convenient!
  • postCreateCommand: A command that will run after the container is created and started for the first time (e.g., npm install to install project dependencies).

Step 4: Open the Project in a Dev Container

After creating the configuration file, VS Code will automatically ask if you want to “Reopen in Container.” Select this option.

The first time, this process will take a moment as Docker needs to pull the image and build the container. Once completed, VS Code will restart. You’ll see an indicator in the bottom-left corner of VS Code showing that you are working inside a Dev Container (e.g., “Dev Container: Node.js Development”).

Open the integrated terminal in VS Code (Ctrl+`). You’ll see the terminal prompt belonging to the container. Try running the command:

node app.js

You will see the output: Hello from Dev Container!. This demonstrates that your code is truly running inside an isolated Docker environment.

Check the Extensions view, and you’ll see extensions like ESLint and Prettier installed and activated inside the container, not on your local machine.

Step 5: Advanced Customization and Personal Experience

You can customize devcontainer.json and Dockerfile to suit any project needs. For instance, if you need a Python environment:

{
  "name": "Python Development",
  "image": "mcr.microsoft.com/devcontainers/python:3.10",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {
      "version": "latest"
    }
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.vscode-pylance"
      ]
    }
  },
  "postCreateCommand": "pip install -r requirements.txt"
}

In this example, I’ve added the docker-in-docker feature. This allows you to run Docker commands inside your Dev Container – extremely useful if your project needs to build or run other containers (e.g., microservices or databases). The necessary Python extensions are also automatically installed.

I recall once debugging an API that returned a very long JSON response. At that time, I hadn’t installed any formatter extensions in my Dev Container. I had to copy the JSON, paste it into toolcraft.app/en/tools/developer/json-formatter to make it readable.

Later, I realized I could simply add esbenp.prettier-vscode or a JSON formatter extension to customizations.vscode.extensions in devcontainer.json for automatic installation. From then on, I always prioritize defining all necessary extensions in this configuration file. This approach is significantly faster, saving about 30 seconds each time, and eliminates the need to open a browser!

Benefits of Using Dev Containers

  • Consistent and reproducible environments: Everyone has an identical development environment, minimizing “works on my machine” errors.
  • Rapid onboarding: New team members can start coding almost immediately, without time-consuming, complex setups.
  • Project isolation: Each project has its own environment, preventing dependency conflicts.
  • Easy experimentation with new technologies: Want to try a new framework or a different Node.js version? Just create a new Dev Container; it won’t affect your local machine.
  • Cloud development environments: Dev Containers also serve as the foundation for cloud development services like GitHub Codespaces, allowing you to write code from any device.

Conclusion

VS Code Dev Containers are more than just a convenient feature. They represent a modern working methodology that thoroughly addresses the problem of inconsistent development environments. By encapsulating the entire environment within a Docker container, Dev Containers deliver unparalleled consistency, reproducibility, and efficiency for every project.

If you’re a junior developer or looking to optimize your team’s development workflow, I highly recommend trying and adopting Dev Containers. They will transform the way you work, allowing you to focus on writing code instead of struggling with environment configurations.

Share: