Mastering Dev Containers in VS Code

Create consistent, portable, and reproducible development environments using Dev Containers

Page content

Developers often face the “works on my machine” dilemma due to dependency mismatches, tool versions, or OS differences. Dev Containers in Visual Studio Code (VS Code) solve this elegantly β€” by letting you develop inside a containerized environment configured specifically for your project.

Modern software development demands consistent, reproducible environments that work across machines and operating systems. Whether you’re working on a Python data science project, a Node.js web application, or a Go microservice, ensuring that every team member has an identical development setup can be challenging.

vs code dev containers

This comprehensive guide walks through what Dev Containers are, why they’re valuable, and how to set them up in VS Code for smooth, portable development workflows. You’ll learn everything from basic setup to advanced configurations with Docker Compose and best practices for team collaboration.


🧩 What Are Dev Containers?

Dev Containers are a feature provided by the VS Code Remote - Containers extension (now part of VS Code Remote Development). They allow you to open your project in a Docker container that’s pre-configured with all your dependencies, languages, and tools.

Think of it as:

“A fully configured development environment, defined as code.”

Instead of installing Python, Node.js, databases, and various tools directly on your machine, you define them in configuration files. When you open the project in VS Code, it automatically spins up a container with everything pre-installed and configured exactly as specified.

A Dev Container setup typically includes:

  • A Dockerfile or reference to a base image (defining the container OS, languages, and tools)
  • A devcontainer.json file (configuring workspace settings, VS Code extensions, port forwarding, environment variables, and startup commands)
  • Optional docker-compose.yml if your project depends on multiple services (like databases, Redis, message queues, etc.)

βš™οΈ Why Use Dev Containers?

Here’s what makes them powerful:

  • Reproducibility: Every developer and CI system uses the exact same environment. No more “it works on my machine but not on yours” issues. What runs on your laptop will run identically on your colleague’s Windows machine, Mac, or Linux workstation.

  • Isolation: No need to pollute your local machine with conflicting dependencies. Work on multiple projects that require different versions of Python, Node.js, or other tools without version conflicts or virtual environment juggling.

  • Portability: Works on any OS that supports Docker. Your development environment travels with your code. Clone a repository, open it in VS Code, and you’re ready to code in minutes β€” regardless of your operating system.

  • Team Consistency: One configuration shared across your entire team. New team members can get up and running in minutes instead of spending hours (or days) configuring their development environment with the right tools and versions.

  • Automation: Automatically installs VS Code extensions, language dependencies, and tools when you open the project. Post-create commands can run database migrations, seed data, or perform other setup tasks without manual intervention.

  • Security: Isolate potentially risky dependencies in containers. If you need to test with an older, vulnerable version of a library, it stays contained and doesn’t affect your host system.

Real-world example: Imagine joining a team working on a microservices project that uses Python 3.11, PostgreSQL 15, Redis, and Elasticsearch. Without Dev Containers, you’d spend hours installing and configuring each component. With Dev Containers, you open the project in VS Code, let it build the container, and you’re writing code within 5-10 minutes.


🧱 Setting Up a Dev Container in VS Code

Let’s go step-by-step.

1. Install the Required Tools

Before you start, ensure you have the following installed:

  • Docker Desktop (or an equivalent container runtime like Podman)

    • For Windows/Mac: Download and install Docker Desktop
    • For Linux: Install Docker Engine and ensure your user is in the docker group
  • VS Code (latest version recommended)

  • The Dev Containers extension (by Microsoft)

    • Open VS Code
    • Go to Extensions (Ctrl+Shift+X or Cmd+Shift+X)
    • Search for “Dev Containers”
    • Install the extension with ID: ms-vscode-remote.remote-containers

Verify your setup:

# Check Docker is running
docker --version
docker ps

# Should output Docker version and running containers (if any)

2. Initialize the Dev Container

Open your project folder in VS Code and open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P on macOS), then type and select:

Dev Containers: Add Dev Container Configuration Files...

VS Code will present a list of predefined environment templates. Choose the one that matches your project:

  • Node.js β€” JavaScript/TypeScript projects
  • Python β€” Data science, web apps, scripts
  • Go β€” Go applications and services
  • .NET β€” C#/F# applications
  • Java β€” Spring Boot, Maven, Gradle projects
  • Docker-in-Docker β€” When you need Docker inside your container
  • And many more…

You can also select additional features like:

  • Common utilities (git, curl, wget)
  • Database clients
  • Cloud CLI tools (AWS, Azure, GCP)

This wizard creates a .devcontainer folder with:

  • devcontainer.json β€” Main configuration file
  • Dockerfile β€” Custom image definition (or a reference to a pre-built base image)

3. Customize devcontainer.json

The devcontainer.json file is where the magic happens. Here’s a well-documented example for a Node.js project:

{
  // Container display name in VS Code
  "name": "Node.js Development Container",
  
  // Build configuration - can use Dockerfile or pre-built image
  "build": {
    "dockerfile": "Dockerfile",
    "context": ".."
  },
  
  // Alternative: use a pre-built image instead of Dockerfile
  // "image": "mcr.microsoft.com/devcontainers/javascript-node:18",
  
  // Workspace configuration
  "customizations": {
    "vscode": {
      // VS Code settings that apply in the container
      "settings": {
        "terminal.integrated.defaultProfile.linux": "bash",
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      },
      
      // Extensions to install automatically
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "eamodio.gitlens",
        "ms-azuretools.vscode-docker"
      ]
    }
  },
  
  // Port forwarding - make container ports available on host
  "forwardPorts": [3000, 5432],
  "portsAttributes": {
    "3000": {
      "label": "Application",
      "onAutoForward": "notify"
    }
  },
  
  // Commands to run at different stages
  "postCreateCommand": "npm install",     // After container is created
  "postStartCommand": "npm run dev",      // After container starts
  
  // Environment variables
  "containerEnv": {
    "NODE_ENV": "development",
    "PORT": "3000"
  },
  
  // Run container as non-root user (recommended for security)
  "remoteUser": "node",
  
  // Mount additional volumes
  "mounts": [
    "source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,readonly,type=bind"
  ]
}

Key configuration options explained:

  • name β€” Display name shown in VS Code status bar
  • build / image β€” Use a Dockerfile or pre-built image
  • customizations.vscode.extensions β€” VS Code extensions to auto-install
  • forwardPorts β€” Ports to expose from container to host
  • postCreateCommand β€” Runs once when container is first created (e.g., install dependencies)
  • postStartCommand β€” Runs every time the container starts
  • containerEnv β€” Environment variables available in the container
  • remoteUser β€” User account to use inside the container
  • mounts β€” Additional files/folders to mount (like SSH keys)

πŸ’‘ Pro Tips:

  • Use postCreateCommand for slow operations (npm install, pip install)
  • Use postStartCommand for fast startup tasks (database migrations)
  • Always specify extensions your project needs β€” this ensures consistent tooling
  • Use environment variables for configuration that differs between developers

4. Build and Open in Container

Once your configuration is ready, it’s time to launch your development environment:

Open Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and run:

Dev Containers: Reopen in Container

What happens next:

  1. Image Building β€” VS Code builds the Docker image based on your Dockerfile or pulls a pre-built image. This may take a few minutes the first time.

  2. Container Creation β€” Docker creates a new container from the built image.

  3. Volume Mounting β€” Your project directory is mounted into the container, making your code accessible inside.

  4. Extensions Installation β€” All specified VS Code extensions are automatically installed in the container.

  5. Post-Create Commands β€” Your postCreateCommand runs (e.g., npm install, pip install -r requirements.txt).

  6. Ready! β€” VS Code reconnects to the container, and you’re now developing inside it.

Verify you’re in the container:

You can confirm you’re working inside the container by opening a terminal and running:

# Check the operating system
uname -a
# Output: Linux ... (container's kernel)

# Check hostname (usually the container ID)
hostname
# Output: abc123def456

# Check running processes
ps aux
# You'll see container processes, not your host system's

Notice the VS Code status bar (bottom-left) now shows: Dev Container: [Your Container Name]

Container lifecycle commands:

  • Rebuild Container β€” Dev Containers: Rebuild Container (when you change Dockerfile)
  • Rebuild Without Cache β€” Dev Containers: Rebuild Container Without Cache (for fresh build)
  • Reopen Locally β€” Dev Containers: Reopen Folder Locally (exit container, work on host)

5. Add Additional Services (Optional)

Real-world applications often depend on databases, caching layers, message queues, or other services. You can use Docker Compose to orchestrate multiple containers.

Example: Full-stack application with Node.js, PostgreSQL, and Redis

Create a docker-compose.yml in your .devcontainer folder:

version: "3.8"

services:
  # Main development container
  app:
    build: 
      context: ..
      dockerfile: .devcontainer/Dockerfile
    
    volumes:
      # Mount project directory
      - ..:/workspace:cached
      # Use named volume for node_modules (better performance)
      - node_modules:/workspace/node_modules
    
    # Keep container running
    command: sleep infinity
    
    # Network access to other services
    depends_on:
      - db
      - redis
    
    environment:
      DATABASE_URL: postgresql://dev:secret@db:5432/appdb
      REDIS_URL: redis://redis:6379

  # PostgreSQL database
  db:
    image: postgres:15-alpine
    restart: unless-stopped
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    ports:
      - "5432:5432"

  # Redis cache
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis-data:/data
    ports:
      - "6379:6379"

volumes:
  postgres-data:
  redis-data:
  node_modules:

Then, update your devcontainer.json to use Docker Compose:

{
  "name": "Full-stack Dev Environment",
  
  // Use docker-compose instead of single container
  "dockerComposeFile": "docker-compose.yml",
  
  // Which service to use as the development container
  "service": "app",
  
  // Path to workspace folder inside container
  "workspaceFolder": "/workspace",
  
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "ms-azuretools.vscode-docker",
        "ckolkman.vscode-postgres"  // PostgreSQL client
      ]
    }
  },
  
  "forwardPorts": [3000, 5432, 6379],
  
  "postCreateCommand": "npm install && npm run db:migrate",
  
  "remoteUser": "node"
}

What this setup provides:

  • app β€” Your development container with Node.js
  • db β€” PostgreSQL database, accessible at db:5432 from your app
  • redis β€” Redis cache, accessible at redis:6379
  • Named volumes β€” Persist database data between container restarts
  • Port forwarding β€” Access all services from your host machine

Connect to services from your code:

// In your Node.js application
const { Pool } = require('pg');
const redis = require('redis');

// PostgreSQL connection
const pool = new Pool({
  connectionString: process.env.DATABASE_URL
  // Resolves to: postgresql://dev:secret@db:5432/appdb
});

// Redis connection
const redisClient = redis.createClient({
  url: process.env.REDIS_URL
  // Resolves to: redis://redis:6379
});

Access services from your host:

  • App: http://localhost:3000
  • PostgreSQL: localhost:5432 (using any PostgreSQL client)
  • Redis: localhost:6379 (using redis-cli or GUI tools)

Now, when you open the project in VS Code, all services start together automatically!


🧠 Advanced Tips and Best Practices

Use Pre-Built Images

Save significant build time by starting from Microsoft’s official devcontainer images:

{
  "image": "mcr.microsoft.com/devcontainers/python:3.11",
  "features": {
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {}
  }
}

Features are reusable installation scripts for common tools (Git, GitHub CLI, Node, AWS CLI, etc.).

Version Control Best Practices

Always commit your .devcontainer folder:

git add .devcontainer/
git commit -m "Add Dev Container configuration"
git push

This ensures:

  • βœ… New team members get the environment automatically
  • βœ… Environment changes are tracked and reviewable
  • βœ… Everyone develops in the same setup

Pro tip: Add a README section explaining the dev container setup:

## Development Setup

This project uses VS Code Dev Containers. To get started:

1. Install Docker Desktop and VS Code
2. Install the "Dev Containers" extension
3. Clone this repository
4. Open in VS Code
5. Click "Reopen in Container" when prompted

Debugging in Containers

Debugging works seamlessly. Configure your launch.json as usual:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Launch Node.js",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/index.js",
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

Set breakpoints and debug normally β€” VS Code handles the container connection automatically.

Continuous Integration Parity

Use the same container image in your CI/CD pipeline:

# GitHub Actions example
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: mcr.microsoft.com/devcontainers/javascript-node:18
    steps:
      - uses: actions/checkout@v3
      - run: npm install
      - run: npm test

This ensures dev/prod parity β€” if tests pass locally, they’ll pass in CI.

Performance Optimization

For macOS/Windows users β€” use named volumes for dependencies:

{
  "mounts": [
    "source=myproject-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
  ]
}

This significantly improves file I/O performance for node_modules, venv, etc.

Multi-Stage Development

Create different configurations for different team roles:

.devcontainer/
β”œβ”€β”€ devcontainer.json          # Default (full-stack)
β”œβ”€β”€ frontend/
β”‚   └── devcontainer.json      # Frontend-only (lighter)
└── backend/
    └── devcontainer.json      # Backend-only (with DB)

Team members can choose their environment when opening the project.

Working with SSH Keys and Git

Mount your SSH keys for Git operations:

{
  "mounts": [
    "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/node/.ssh,readonly,type=bind"
  ],
  "postCreateCommand": "ssh-add ~/.ssh/id_ed25519 || true"
}

Custom Environment Files

Load environment-specific configuration:

{
  "runArgs": ["--env-file", ".devcontainer/.env"]
}

.devcontainer/.env:

API_KEY=dev_key_here
DEBUG=true
LOG_LEVEL=debug

πŸ”§ Common Troubleshooting

Container Won’t Start

Error: Cannot connect to the Docker daemon

Solution:

  • Ensure Docker Desktop is running
  • On Linux, check: sudo systemctl status docker
  • Verify Docker is in your PATH: docker --version

Slow Performance on macOS/Windows

Issue: File operations are slow

Solutions:

  1. Use named volumes for node_modules, venv, etc.

  2. Enable file sharing in Docker Desktop settings

  3. Consider using cached or delegated mount options:

    "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"
    

Extensions Not Installing

Issue: Extensions specified in devcontainer.json don’t install

Solutions:

  1. Rebuild container: Dev Containers: Rebuild Container
  2. Check extension IDs are correct
  3. Ensure extensions support remote containers (most do)

Port Already in Use

Error: Port 3000 is already allocated

Solutions:

  1. Stop conflicting containers: docker ps and docker stop <container>
  2. Change port mapping in forwardPorts
  3. Use dynamic ports: VS Code will auto-assign available ports

Changes to Dockerfile Not Applied

Issue: Modified Dockerfile but changes don’t appear

Solution: Rebuild without cache:

Dev Containers: Rebuild Container Without Cache

Container Exits Immediately

Issue: Container starts then stops

Solution: Add a command to keep it running in docker-compose.yml:

command: sleep infinity

Or in devcontainer.json:

{
  "overrideCommand": true
}

βœ… Conclusion

Dev Containers in VS Code bring consistency, simplicity, and automation to your development workflow. They turn complex, fragile setups into code-defined environments that just work, regardless of your machine or operating system.

Key takeaways:

  • 🎯 Eliminate “works on my machine” problems β€” Everyone uses identical environments
  • πŸš€ Faster onboarding β€” New team members productive in minutes, not days
  • πŸ”’ Better security β€” Isolate dependencies from your host system
  • πŸ“¦ Portable β€” Your environment travels with your code
  • 🀝 Team consistency β€” No more dependency version conflicts
  • πŸ”„ CI/CD parity β€” Use the same image in development and continuous integration

Whether you’re working on a simple Python script or a complex microservices architecture with multiple databases, Dev Containers provide a robust foundation for modern development.

If you collaborate on multi-language projects, contribute to open-source repositories, onboard new developers frequently, or simply want clean and reproducible dev environments β€” Dev Containers are a must-have tool in your stack.

Start small: try Dev Containers on your next project. Once you experience the benefits, you’ll wonder how you ever developed without them.


Official Documentation:

Related Articles on This Site: