Devcontainers for Reproducible Development

Dev Containers define your entire development environment as code, ensuring every team member runs the same tools, dependencies, and settings regardless of their host OS. This guide covers creating devcontainer.json configurations, using Dev Container Features, Docker Compose integration, and running Dev Containers from both VS Code and the CLI.

Prerequisites

  • Docker installed and running
  • VS Code with the Dev Containers extension (for VS Code usage)
  • Node.js 18+ (for the Dev Container CLI)
  • Basic familiarity with Docker

Understanding Dev Containers

A Dev Container is a Docker container configured via .devcontainer/devcontainer.json at the root of your repository. When you open the project in VS Code (or use the CLI), it:

  1. Builds or pulls the container image
  2. Mounts your project files into the container
  3. Installs VS Code extensions inside the container
  4. Runs any post-create setup commands

Your code runs inside the container, but your files stay on the host.

Creating Your First devcontainer.json

mkdir -p .devcontainer
// .devcontainer/devcontainer.json
{
  "name": "My Project Dev Container",

  // Use a pre-built image
  "image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye",

  // Forward ports from container to host
  "forwardPorts": [8000, 5432],

  // VS Code extensions to install inside the container
  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.black-formatter",
        "ms-python.pylint",
        "esbenp.prettier-vscode"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python",
        "editor.formatOnSave": true
      }
    }
  },

  // Commands to run after container creation
  "postCreateCommand": "pip install -r requirements.txt",

  // Set the remote user (avoids running as root)
  "remoteUser": "vscode"
}

Using Dev Container Features

Features are self-contained scripts that install tools into your container without writing custom Dockerfiles:

{
  "name": "Node.js Project",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04",

  "features": {
    // Node.js LTS
    "ghcr.io/devcontainers/features/node:1": {
      "version": "20"
    },
    // Docker-in-Docker for building images
    "ghcr.io/devcontainers/features/docker-in-docker:2": {
      "version": "latest"
    },
    // Git with latest version
    "ghcr.io/devcontainers/features/git:1": {
      "version": "latest",
      "ppa": true
    },
    // GitHub CLI
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },

  "postCreateCommand": "npm install",

  "customizations": {
    "vscode": {
      "extensions": ["esbenp.prettier-vscode", "ms-vscode.vscode-typescript-next"]
    }
  }
}

Browse available features at containers.dev/features.

Custom Dockerfiles

When you need more control, reference a Dockerfile:

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

# Install system dependencies
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    redis-tools \
    && rm -rf /var/lib/apt/lists/*

# Install specific Go version
RUN curl -fsSL https://go.dev/dl/go1.21.0.linux-amd64.tar.gz \
    | tar -C /usr/local -xzf -
ENV PATH=$PATH:/usr/local/go/bin

# Install project tools
RUN go install github.com/air-verse/air@latest

USER vscode

Reference it in devcontainer.json:

{
  "name": "Go Project",
  "build": {
    "dockerfile": "Dockerfile",
    "context": "..",
    "args": {
      "GO_VERSION": "1.21"
    }
  },
  "postCreateCommand": "go mod download",
  "forwardPorts": [8080]
}

Docker Compose Integration

For projects with multiple services (app + database + cache):

# .devcontainer/docker-compose.yml
version: '3.8'
services:
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - ..:/workspace:cached
      - /var/run/docker.sock:/var/run/docker.sock
    command: sleep infinity  # Keep container running
    depends_on:
      - db
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpassword
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  postgres-data:
// .devcontainer/devcontainer.json
{
  "name": "Full Stack Dev",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",                        // Which service is the dev container
  "workspaceFolder": "/workspace",

  "forwardPorts": [3000, 5432, 6379],

  "postCreateCommand": "npm install && npx prisma migrate dev",

  "customizations": {
    "vscode": {
      "extensions": ["ms-vscode.vscode-node-azure-pack"]
    }
  }
}

VS Code Usage

  1. Install the Dev Containers extension (ms-vscode-remote.remote-containers)
  2. Open your repository in VS Code
  3. Click the green >< button in the bottom-left corner
  4. Select Reopen in Container

VS Code builds the container, installs extensions, and reopens in the container. The integrated terminal runs inside the container.

Useful commands (via Command Palette Ctrl+Shift+P):

  • Dev Containers: Rebuild Container - Rebuild after Dockerfile changes
  • Dev Containers: Open Container Configuration File - Edit devcontainer.json
  • Dev Containers: Show Container Log - Debug build issues
  • Dev Containers: Clone Repository in Container Volume - For better performance on macOS

Dev Container CLI Usage

For CI/CD or non-VS Code workflows:

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

# Build and start the dev container
devcontainer up --workspace-folder .

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

# Build only (for CI caching)
devcontainer build --workspace-folder . --image-name myapp-devcontainer:latest

# Use in CI (GitHub Actions example)

GitHub Actions workflow using a Dev Container:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install Dev Container CLI
        run: npm install -g @devcontainers/cli
      - name: Run tests in dev container
        run: |
          devcontainer up --workspace-folder .
          devcontainer exec --workspace-folder . npm test

Team Standardization

Best practices for consistent team environments:

{
  // Pin image versions for reproducibility
  "image": "mcr.microsoft.com/devcontainers/python:3.11.6-bullseye",

  // Mount SSH agent for Git operations
  "mounts": [
    "source=${localEnv:SSH_AUTH_SOCK},target=/ssh-agent,type=bind"
  ],
  "remoteEnv": {
    "SSH_AUTH_SOCK": "/ssh-agent"
  },

  // Configure Git inside container
  "postCreateCommand": "git config --global core.autocrlf false",

  // Lifecycle scripts
  "postStartCommand": "git fetch --all",

  "initializeCommand": "echo 'Starting dev environment...'",

  // Share dotfiles
  "dotfiles": {
    "repository": "https://github.com/yourteam/dotfiles",
    "installCommand": "install.sh"
  }
}

Troubleshooting

Container fails to build:

# Check build logs in VS Code: Command Palette > "Dev Containers: Show Container Log"

# Or via CLI with verbose output
devcontainer up --workspace-folder . --log-level debug

Extensions not installing:

# Verify extension IDs are correct (find on marketplace URL)
# Check internet connectivity from inside container:
devcontainer exec --workspace-folder . curl -I https://marketplace.visualstudio.com

File permission issues:

# Add to devcontainer.json to fix ownership
"postCreateCommand": "sudo chown -R vscode:vscode /workspace"

Slow file I/O on macOS:

Use named volumes for the workspace mount to bypass macOS file sharing overhead:

{
  "workspaceMount": "source=myproject-volume,target=/workspace,type=volume",
  "workspaceFolder": "/workspace"
}

Conclusion

Dev Containers eliminate "works on my machine" problems by encoding the entire development environment as version-controlled configuration. By combining Features for tool installation, Docker Compose for multi-service setups, and the Dev Container CLI for CI integration, teams achieve consistent, reproducible environments from day one. Store devcontainer.json in source control so new team members are productive in minutes.