Skip to content

Cloud & Infrastructure · Developer Experience

Dev Containers: Reproducible Development Environments in 2026

Dev Containers let you define your entire development environment in a JSON file. New team members are productive in minutes, not days. Here's how to set them up properly.

Anurag Verma

Anurag Verma

6 min read

Dev Containers: Reproducible Development Environments in 2026

Sponsored

Share

“Works on my machine” is a tired joke, but the underlying problem is real. Onboarding a new developer still takes hours of installing tools, resolving version conflicts, and debugging environment-specific issues that have nothing to do with the actual work. Dev Containers are the most practical solution to this problem that’s gained genuine adoption.

The spec is simple: a .devcontainer/devcontainer.json file that describes a container-based development environment. VS Code, JetBrains, GitHub Codespaces, and several other tools can all read this file and spin up an identical environment for everyone on the team.

What You Get

When you open a project with a devcontainer in VS Code (after installing the Dev Containers extension), VS Code:

  1. Builds or pulls the container image
  2. Mounts your project files into the container
  3. Installs any VS Code extensions you’ve specified
  4. Runs any post-creation setup scripts
  5. Attaches a VS Code window to the container

From that point, your terminal, language servers, formatters, linters, and debuggers all run inside the container. Your host machine doesn’t need Node, Python, Rust, or any other runtime. It just needs Docker and VS Code.

A Minimal Example

The simplest devcontainer uses a pre-built Microsoft image:

// .devcontainer/devcontainer.json
{
  "name": "Node.js Project",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:22",
  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "bradlc.vscode-tailwindcss"
      ],
      "settings": {
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.formatOnSave": true
      }
    }
  },
  "postCreateCommand": "npm install"
}

That’s it. Open the project, VS Code prompts “Reopen in Container,” and 60 seconds later you have a fully configured Node 22 environment with ESLint, Prettier, and the GitHub CLI already installed.

Using a Custom Dockerfile

Pre-built images cover common setups, but real projects have specific needs. You can point the devcontainer at a custom Dockerfile:

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

# Install specific Node version via fnm
RUN curl -fsSL https://fnm.vercel.app/install | bash \
    && fnm install 22.4.0 \
    && fnm default 22.4.0

# Install pnpm
RUN npm install -g pnpm@9

# Install Python for scripts
RUN apt-get update && apt-get install -y python3 python3-pip

# Install project-specific system dependencies
RUN apt-get install -y postgresql-client redis-tools

USER vscode
// .devcontainer/devcontainer.json
{
  "name": "My App",
  "build": {
    "dockerfile": "Dockerfile"
  },
  "runArgs": ["--init"],
  "mounts": [
    "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly"
  ],
  "remoteEnv": {
    "NODE_ENV": "development"
  },
  "postCreateCommand": "pnpm install",
  "postStartCommand": "pnpm run db:migrate"
}

The mounts field is useful for SSH keys. Your container can use your host’s SSH configuration without copying keys into the image.

Adding a Database with Docker Compose

Most real applications need a database alongside the app server. Docker Compose handles this:

# .devcontainer/docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - ../..:/workspaces:cached
    command: sleep infinity
    environment:
      DATABASE_URL: postgres://dev:dev@db:5432/appdb
      REDIS_URL: redis://cache:6379
    depends_on:
      - db
      - cache

  db:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: appdb
    volumes:
      - postgres-data:/var/lib/postgresql/data

  cache:
    image: redis:7-alpine
    restart: unless-stopped

volumes:
  postgres-data:
// .devcontainer/devcontainer.json
{
  "name": "Full Stack App",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "ms-azuretools.vscode-docker",
        "mtxr.sqltools",
        "mtxr.sqltools-driver-pg"
      ]
    }
  },
  "postCreateCommand": "npm install && npm run db:migrate"
}

Every developer on the team gets an identical Postgres 16 and Redis 7 setup. No installation instructions, no “which version of Postgres do you have?” debugging.

Dev Container Features

The devcontainer features spec provides reusable environment add-ons. Instead of writing Dockerfile commands to install common tools, you reference a feature:

{
  "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/aws-cli:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/terraform:1": {
      "version": "1.9"
    }
  }
}

Features are composable and versioned. The community maintains a catalog of them at containers.dev/features.

GitHub Codespaces

The same devcontainer.json file powers GitHub Codespaces, which are cloud-hosted development environments accessible from any browser.

When you open a Codespace on GitHub (Code button → Codespaces → Create codespace), GitHub reads your devcontainer config, provisions a VM, builds the container, and gives you a VS Code instance in the browser. The whole process takes about a minute for cached images.

Codespaces machine types in 2026 go from 2-core/8GB up to 32-core/64GB. For most development work, the 4-core/16GB option is adequate.

Codespaces integrates with GitHub secrets. You can add environment variables and secrets in your repository settings, and they’re automatically available in your Codespace without ever touching a .env file manually.

Useful for:

  • Reviewing PRs in a full environment, not just reading diffs
  • Trying out open source projects without touching your local setup
  • Giving demo or test access to contractors without dealing with VPNs
  • Onboarding: new hires can contribute on day one without any local setup

Secrets and Environment Variables

Don’t put secrets in devcontainer.json. Instead, use a local .env file that’s gitignored, and reference it from the config:

{
  "runArgs": ["--env-file", "${localWorkspaceFolder}/.env.local"]
}

Or for Codespaces, use repository secrets (Settings → Secrets → Codespaces). They’re injected as environment variables automatically.

For local development, a .env.example file committed to the repo (with placeholder values, not real secrets) documents what’s needed. New team members copy it to .env.local and fill in their values.

Performance on macOS

Docker on macOS runs Linux containers inside a VM, which introduces file system latency for bind mounts. This is noticeable for projects with thousands of files and heavy filesystem operations (e.g., node_modules).

Two approaches help:

Use named volumes for node_modules:

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

This keeps node_modules in Docker’s native Linux filesystem rather than syncing through the macOS bridge. File system operations inside the container are much faster.

Use VirtioFS (Docker Desktop 4.6+):

Docker Desktop’s VirtioFS file sharing is significantly faster than the older gRPC-FUSE implementation. Enable it in Docker Desktop → Settings → General → “Use VirtioFS for file sharing.”

When It’s Worth It

Dev containers add a layer of complexity. They’re worth it when:

  • Teams are larger than 3-4 people. Environment drift compounds with team size.
  • Onboarding frequency is high. Contractors, rotating contributors, and open source projects benefit most.
  • The project has complex system dependencies. If setup requires more than 15 minutes of documentation, a devcontainer pays for itself the first time someone follows that doc.
  • You use GitHub Actions. Your CI environment can also use the devcontainer image directly (devcontainer/ci action), so local and CI environments are identical.

For solo projects on a stable setup, the overhead isn’t worth it. For a team project where developers join and leave, or where the setup instructions document requires more than three steps, a devcontainer is the right investment.

Quick Start

  1. Install the VS Code Dev Containers extension and Docker Desktop.
  2. Add .devcontainer/devcontainer.json with a suitable base image.
  3. Open the command palette → “Dev Containers: Reopen in Container.”
  4. Add your project’s post-create setup steps to postCreateCommand.
  5. Commit the .devcontainer/ folder to the repository.

The first open takes a few minutes to build the image. Subsequent opens use the cached image and are much faster.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored