← All articles
CONTAINERS Dev Containers: Reproducible Development Environment... 2026-02-14 · 5 min read · devcontainers · docker · vscode

Dev Containers: Reproducible Development Environments That Actually Work

Containers 2026-02-14 · 5 min read devcontainers docker vscode containers development-environment reproducibility codespaces

Dev Containers: Reproducible Development Environments That Actually Work

Every team has the same problem: a new developer joins, spends a day installing the right versions of Node, Python, Postgres, Redis, and a dozen other tools, then hits a platform-specific bug that takes another half-day to resolve. Dev Containers solve this by defining your entire development environment -- runtime, tools, extensions, settings -- in a JSON file that produces an identical container on any machine.

Dev Containers development environments

The Dev Container specification is an open standard, originally created by Microsoft for VS Code, now supported by GitHub Codespaces, JetBrains IDEs, DevPod, and the devcontainer CLI. This guide covers practical setup, customization, and the patterns that make Dev Containers worth the initial investment.

How It Works

A Dev Container is a Docker container configured for development. Your editor connects to the container and runs your code inside it. The project files are mounted from your host machine (or cloned into the container), so you edit locally but execute in a controlled environment.

The configuration lives in .devcontainer/devcontainer.json at the root of your repository. When you open the project in VS Code (or any supporting tool), it reads this file, builds or pulls the container, and connects to it.

Minimal Setup

Basic devcontainer.json

{
  "name": "My Project",
  "image": "mcr.microsoft.com/devcontainers/typescript-node:22",
  "postCreateCommand": "npm install",
  "customizations": {
    "vscode": {
      "extensions": [
        "biomejs.biome",
        "bradlc.vscode-tailwindcss"
      ]
    }
  }
}

This does three things:

  1. Uses a pre-built image with Node.js 22 and TypeScript tooling
  2. Runs npm install after the container is created
  3. Installs VS Code extensions inside the container

Create this file, open the project in VS Code, and click "Reopen in Container" when prompted. Everything else happens automatically.

Using a Custom Dockerfile

For more control, use a Dockerfile:

.devcontainer/
  devcontainer.json
  Dockerfile
# .devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/base:ubuntu

# Install project-specific tools
RUN apt-get update && apt-get install -y \
    postgresql-client \
    redis-tools \
    && rm -rf /var/lib/apt/lists/*

# Install Bun
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
{
  "name": "My Project",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "postCreateCommand": "bun install",
  "customizations": {
    "vscode": {
      "extensions": [
        "biomejs.biome"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "biomejs.biome"
      }
    }
  }
}

Dev Container Features

Features are reusable, shareable units of configuration. Instead of writing Dockerfile instructions for common tools, you declare features:

{
  "name": "Full Stack Project",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "features": {
    "ghcr.io/devcontainers/features/node:1": {
      "version": "22"
    },
    "ghcr.io/devcontainers/features/python:1": {
      "version": "3.12"
    },
    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/aws-cli:1": {}
  }
}

Each feature installs and configures a specific tool. This is composable -- add the features your project needs without maintaining a complex Dockerfile. The official features registry at ghcr.io/devcontainers/features covers most common development tools.

Community features extend the ecosystem further:

{
  "features": {
    "ghcr.io/devcontainers-contrib/features/bun:1": {},
    "ghcr.io/devcontainers-contrib/features/mise-asdf:1": {},
    "ghcr.io/devcontainers-extra/features/playwright:1": {}
  }
}

Multi-Container Setup with Docker Compose

Real applications need databases, caches, and other services. Use Docker Compose to run them alongside your dev container:

# .devcontainer/docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ..:/workspace:cached
    command: sleep infinity
    depends_on:
      - db
      - redis

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:
{
  "name": "Full Stack App",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
  "postCreateCommand": "npm install && npm run db:migrate",
  "forwardPorts": [3000, 5432, 6379],
  "customizations": {
    "vscode": {
      "extensions": [
        "biomejs.biome",
        "ckolkman.vscode-postgres"
      ]
    }
  }
}

When VS Code opens this project, it starts Postgres, Redis, and the development container together. Port forwarding makes the services accessible from your host machine for tools like database GUIs.

Lifecycle Commands

Dev Containers support several lifecycle hooks:

{
  "initializeCommand": "echo 'Running on host before container starts'",
  "onCreateCommand": "echo 'Container created for the first time'",
  "updateContentCommand": "echo 'Runs after clone/pull in container'",
  "postCreateCommand": "npm install && npm run db:setup",
  "postStartCommand": "echo 'Container started (every time)'",
  "postAttachCommand": "echo 'Editor attached to container'"
}

The most commonly used:

Environment Variables and Secrets

{
  "containerEnv": {
    "NODE_ENV": "development",
    "DATABASE_URL": "postgres://dev:dev@db/myapp"
  },
  "remoteEnv": {
    "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}"
  }
}

containerEnv sets environment variables inside the container. remoteEnv pulls values from the host machine, which is useful for secrets you do not want to commit. The ${localEnv:VAR} syntax reads from the host's environment.

For secrets that vary per developer, use a .env file that is gitignored:

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

Practical Configuration Patterns

Python Project

{
  "name": "Python API",
  "image": "mcr.microsoft.com/devcontainers/python:3.12",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "postCreateCommand": "pip install uv && uv sync",
  "customizations": {
    "vscode": {
      "extensions": [
        "charliermarsh.ruff",
        "ms-python.python",
        "ms-python.mypy-type-checker"
      ],
      "settings": {
        "python.defaultInterpreterPath": ".venv/bin/python",
        "[python]": {
          "editor.defaultFormatter": "charliermarsh.ruff",
          "editor.formatOnSave": true
        }
      }
    }
  }
}

Rust Project

{
  "name": "Rust Project",
  "image": "mcr.microsoft.com/devcontainers/rust:latest",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "postCreateCommand": "cargo build",
  "customizations": {
    "vscode": {
      "extensions": [
        "rust-lang.rust-analyzer",
        "vadimcn.vscode-lldb",
        "tamasfe.even-better-toml"
      ],
      "settings": {
        "rust-analyzer.check.command": "clippy"
      }
    }
  }
}

Go Project

{
  "name": "Go Service",
  "image": "mcr.microsoft.com/devcontainers/go:1.22",
  "postCreateCommand": "go mod download",
  "customizations": {
    "vscode": {
      "extensions": ["golang.go"],
      "settings": {
        "go.lintTool": "golangci-lint"
      }
    }
  }
}

The devcontainer CLI

You do not need VS Code to use Dev Containers. The CLI works standalone:

# Install the CLI
npm install -g @devcontainers/cli

# Build the container
devcontainer build --workspace-folder .

# Start and connect
devcontainer up --workspace-folder .

# Run a command inside the container
devcontainer exec --workspace-folder . npm test

# Use in CI
devcontainer exec --workspace-folder . bash -c "npm ci && npm test && npm run build"

This makes Dev Containers useful in CI pipelines -- your CI environment matches your development environment exactly.

Performance Considerations

Dev Containers add overhead. Here is how to minimize it:

File system performance: On macOS and Windows, Docker's file system mount is slow. Use the :cached mount flag or, better, clone the repository inside the container volume:

{
  "workspaceMount": "",
  "workspaceFolder": "/workspace",
  "initializeCommand": "git clone https://github.com/org/repo /tmp/repo || true",
  "postCreateCommand": "cp -r /tmp/repo /workspace && npm install"
}

Image caching: Use a specific image tag rather than latest to leverage Docker's layer cache. Pre-build images with your dependencies installed for faster startup.

Pre-building: For teams, pre-build Dev Container images in CI and push to a registry:

devcontainer build \
  --workspace-folder . \
  --image-name ghcr.io/myorg/devcontainer:latest \
  --push true

Then reference the pre-built image in devcontainer.json for instant startup.

When Dev Containers Are Worth It

Use Dev Containers when:

Skip Dev Containers when:

The Bottom Line

Dev Containers trade a one-time configuration investment for eliminating environment setup problems permanently. The devcontainer.json file becomes the single source of truth for what your project needs to run, and every developer gets an identical environment regardless of their host OS. If your team's onboarding document is longer than half a page of setup instructions, a Dev Container will pay for itself immediately.