V1
Back to handbooks index
Complete Reference
VS Code · Codespaces 2025–2026 containers.dev
complete reference · 2025–2026

Dev Containers What · Why · When · How — with production-ready samples

Development containers give every engineer on your team an identical, reproducible, fully-configured development environment — regardless of their host OS, installed tools, or machine. Defined as code, rebuilt in seconds, and version-controlled with the project.

VS Code GitHub Codespaces JetBrains CLI (devcontainer) Open Standard Node · Python · .NET · Go
Problem & Solution

Why Dev Containers?

The classic "works on my machine" problem destroys developer productivity. Onboarding takes days. OS-specific bugs hide in production. Subtle version differences between engineers cause mysterious test failures. Dev containers solve all of this.

🏗️
Identical Environments

Every developer gets precisely the same runtime, tools, extensions, and configuration. No manual setup steps. No "install Node 20 but some people have 18" drift.

Instant Onboarding

A new engineer clones the repo, opens VS Code, clicks "Reopen in Container" and has a fully functional development environment in minutes — not days.

🔒
Host Isolation

Project tools, runtimes, and dependencies live inside the container. Your host machine stays clean. Switching between projects with conflicting requirements is trivial.

📋
Environment as Code

The dev environment is defined in .devcontainer/devcontainer.json — committed to version control, code-reviewed, and updated just like application code.

☁️
Cloud & Remote Ready

The same devcontainer.json powers GitHub Codespaces, VS Code Remote SSH, and CI pipelines — one definition, used everywhere.

🔄
Reproducible Builds

Pin the exact image, tool versions, and extension list. Check out any historical commit and recreate its exact development environment years later.

Traditional Setup vs Dev Container

DimensionTraditional SetupDev Container
Onboarding timeHours to days of manual stepsMinutes — clone and reopen
Environment parity"Works on my machine" syndromeGuaranteed identical for all developers
Dependency conflictsGlobal tool versions clash between projectsIsolated per project inside container
DocumentationREADME setup guides that go staledevcontainer.json — always current, executable
OS compatibilityDifferent scripts for Mac, Linux, WindowsSingle definition works everywhere
CI parityCI environment differs from localSame container used locally and in CI
Extension consistencyEach dev has different VS Code extensionsExtensions defined and auto-installed
Host machine healthAccumulates global tools, versions, clutterHost stays clean — tools in container
Decision Guide

When To Use Dev Containers

✓ Strong fit — use dev containers when
  • Team spans multiple OS (Windows, macOS, Linux)
  • Project has specific runtime/tool version requirements
  • Onboarding new engineers frequently
  • Project has complex dependencies (databases, queues, etc.)
  • You use GitHub Codespaces for cloud development
  • CI should use the same environment as local dev
  • You maintain multiple projects with conflicting tool versions
  • Security posture benefits from isolating project from host
  • You want extensions and editor settings standardised

How Dev Containers Work

When you open a project in VS Code with a .devcontainer/devcontainer.json, the Dev Containers extension builds (or pulls) a Docker image, starts a container, mounts your project source code into it, and then runs VS Code Server inside the container. Your local VS Code connects to this remote process — giving you full IntelliSense, debugging, and terminal access running inside the container environment.

architecture overview
┌─ Your Machine (host) ──────────────────────────────────────────┐
│                                                                 │
│   VS Code UI ←──── local rendering ────────────────────────┐  │
│         ↕  (extension protocol)                             │  │
│   ┌─ Docker Container ─────────────────────────────────┐   │  │
│   │                                                     │   │  │
│   │   VS Code Server (runs inside container)            │   │  │
│   │   ├─ Language servers (LSP)                         │   │  │
│   │   ├─ Debugger                                       │   │  │
│   │   ├─ Terminal (bash/zsh inside container)           │   │  │
│   │   └─ Extensions (installed inside container)        │   │  │
│   │                                                     │   │  │
│   │   /workspaces/my-project ←── bind mount ──────────────── │  │
│   │   Node 22, Python 3.12, .NET 8, Go 1.23, etc.      │   │  │
│   │   git, gh CLI, custom tools                         │   │  │
│   └─────────────────────────────────────────────────────┘   │  │
│                                                                 │
│   Source files stay on host disk — edited locally by VS Code  │
└─────────────────────────────────────────────────────────────────┘
Project Layout

File Structure

The Dev Container specification looks for configuration in these locations, in order of precedence. Most projects use .devcontainer/devcontainer.json.

file locations (precedence order)
project-root/
├── .devcontainer/
│   ├── devcontainer.json          # ← primary config (most common)
│   ├── Dockerfile                 # custom image (optional)
│   ├── docker-compose.yml         # multi-service (optional)
│   └── scripts/
│       └── post-create.sh         # setup script called by postCreateCommand
│
├── .devcontainer/name/            # multiple named configs (pick at open time)
│   └── devcontainer.json
│
├── .devcontainer.json             # root-level (single file, less common)
│
└── src/ ...                       # your project source
Configuration

devcontainer.json Reference

The devcontainer.json file is a JSON-with-Comments (JSONC) file. It describes the container image, tools to install, VS Code extensions, port forwarding, lifecycle scripts, and more.

json — complete annotated devcontainer.json
{
  "name": "My Project Dev",              // Display name in VS Code UI

  // ── IMAGE / BUILD SOURCE (pick one) ─────────────────
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  // OR
  "build": {
    "dockerfile": "Dockerfile",
    "context": "..",
    "args": { "VARIANT": "22-bookworm" }
  },
  // OR for Docker Compose
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "runServices": ["db", "redis"],

  // ── FEATURES ────────────────────────────────────────
  "features": {
    "ghcr.io/devcontainers/features/node:1": { "version": "22" },
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },

  // ── WORKSPACE ────────────────────────────────────────
  "workspaceFolder": "/workspaces/my-project",

  // ── PORT FORWARDING ──────────────────────────────────
  "forwardPorts": [3000, 5432, 6379],
  "portsAttributes": {
    "3000": { "label": "App", "onAutoForward": "openBrowser" },
    "5432": { "label": "PostgreSQL", "onAutoForward": "silent" }
  },

  // ── ENVIRONMENT ──────────────────────────────────────
  "containerEnv": {
    "NODE_ENV": "development",
    "DATABASE_URL": "postgresql://user:pass@db:5432/mydb"
  },
  "remoteEnv": {
    "LOCAL_WORKSPACE": "${localWorkspaceFolder}"  // host path
  },

  // ── USER ─────────────────────────────────────────────
  "remoteUser": "node",        // run VS Code Server as this user
  "updateRemoteUserUID": true,  // sync UID/GID with host (Linux)

  // ── MOUNTS ───────────────────────────────────────────
  "mounts": [
    "source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,readonly",
    "source=my-volume,target=/data,type=volume"
  ],

  // ── LIFECYCLE SCRIPTS ────────────────────────────────
  "onCreateCommand": "npm install",
  "updateContentCommand": "npm install",
  "postCreateCommand": "bash .devcontainer/scripts/post-create.sh",
  "postStartCommand": "git config core.autocrlf false",
  "postAttachCommand": { // parallel execution (object form)
    "info": "echo 'Container ready!'",
    "check": "node --version"
  },

  // ── VS CODE CUSTOMIZATION ────────────────────────────
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "ms-azuretools.vscode-docker"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "terminal.integrated.defaultProfile.linux": "bash"
      }
    }
  },

  // ── CONTAINER OPTIONS ────────────────────────────────
  "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"],
  "hostRequirements": { "memory": "4gb", "cpus": 2 }
}

Key Properties Reference

PropertyTypePurpose
namestringDisplay name shown in VS Code and Codespaces UI
imagestringDocker image reference — DockerHub, GHCR, MCR, ACR
build.dockerfilestringPath to Dockerfile relative to devcontainer.json
dockerComposeFilestring | arrayDocker Compose file(s) for multi-service setups
servicestringWhich Compose service VS Code connects to
featuresobjectAdd tooling Features from the OCI registry
forwardPortsnumber[]Ports auto-forwarded to localhost on open
portsAttributesobjectLabels and auto-forward behavior per port
containerEnvobjectStatic env vars baked into the container at creation
remoteEnvobjectDynamic env vars for terminals and VS Code processes only
remoteUserstringUser VS Code Server and terminals run as inside container
mountsstring[]Additional bind mounts or named volumes
workspaceFolderstringPath inside container where VS Code opens (default: /workspaces/name)
customizations.vscode.extensionsstring[]Extensions auto-installed in the container
customizations.vscode.settingsobjectVS Code settings scoped to this container
runArgsstring[]Extra Docker run arguments (capabilities, security opts)
hostRequirementsobjectMinimum host memory/CPU — used by Codespaces for sizing
updateRemoteUserUIDbooleanSync container user UID/GID with host user (Linux only)

Variable Substitution

VariableResolves to
${localWorkspaceFolder}Path to project folder on host machine
${containerWorkspaceFolder}Path to project inside the container
${localEnv:HOME}Host environment variable (e.g., /home/alice)
${containerEnv:PATH}Container environment variable
${localWorkspaceFolderBasename}Project folder name only (e.g., my-project)
Composability

Dev Container Features

Features are self-contained, shareable units of installation code. Reference them by OCI identifier in "features": {} — no Dockerfile editing needed. They run as a secondary build step and can contribute lifecycle scripts, environment variables, and more.

💡
Features vs Dockerfile: Use Features when the tooling is available as a maintained Feature — they handle OS detection, user setup, and PATH correctly. Use a custom Dockerfile when you need precise control over the build, layer caching, or tooling that doesn't have a Feature.

Popular Official Features

FeatureOCI identifierWhat it installs
Node.jsghcr.io/devcontainers/features/node:1Node.js, npm, nvm. Accepts version, nodeGypDependencies
Pythonghcr.io/devcontainers/features/python:1Python, pip, pyenv. Accepts version
Goghcr.io/devcontainers/features/go:1Go toolchain. Accepts version
Rustghcr.io/devcontainers/features/rust:1Rust, cargo, rustup
Javaghcr.io/devcontainers/features/java:1JDK via SDKMAN. Accepts version, jdkDistro
.NETghcr.io/devcontainers/features/dotnet:2.NET SDK. Accepts version
Gitghcr.io/devcontainers/features/git:1Latest git, git-lfs, config helpers
GitHub CLIghcr.io/devcontainers/features/github-cli:1gh CLI tool
Docker-in-Dockerghcr.io/devcontainers/features/docker-in-docker:2Full Docker daemon inside the container
Docker-outside-Dockerghcr.io/devcontainers/features/docker-outside-docker:1Docker socket bind mount (simpler)
kubectlghcr.io/devcontainers/features/kubectl-helm-minikube:1kubectl, helm, optional minikube
Terraformghcr.io/devcontainers/features/terraform:1Terraform, tflint, terragrunt
Azure CLIghcr.io/devcontainers/features/azure-cli:1az CLI
AWS CLIghcr.io/devcontainers/features/aws-cli:1AWS CLI v2
json — feature configuration examples
"features": {
  // Specific version
  "ghcr.io/devcontainers/features/node:1": {
    "version": "22",
    "nodeGypDependencies": true
  },

  // Latest (shorthand) — equivalent to { "version": "latest" }
  "ghcr.io/devcontainers/features/git:1": "latest",

  // Multiple tools in one file
  "ghcr.io/devcontainers/features/python:1": { "version": "3.12" },
  "ghcr.io/devcontainers/features/github-cli:1": {},
  "ghcr.io/devcontainers/features/terraform:1": { "version": "latest" },

  // Local Feature (during development)
  "./my-local-feature": {}
}
Container Events

Lifecycle Hooks

Lifecycle scripts run at specific points in the container's life. They are executed inside the container from the workspace folder. If a script fails, subsequent scripts are skipped.

Phase 1
initializeCommand
Runs on HOST before container starts. Use for host-side setup only.
Phase 2
onCreateCommand
Runs once when container is first created. Install tools, one-time setup.
Phase 3
updateContentCommand
Runs when content changes (Codespaces prebuild). Re-install deps.
Phase 4
postCreateCommand
Runs after creation. npm install, database seed, .env copy, etc.
Phase 5
postStartCommand
Runs every time container starts. Start background services.
HookRuns whenFrequencyUse for
initializeCommandOn host, before container startsEvery startHost-side validation, creating local dirs
onCreateCommandInside container, after creationOnce at creationTool configuration, one-time installs
updateContentCommandAfter content update (prebuilds)When content changesDependency install tied to project files
postCreateCommandAfter all create commands finishOnce at creationnpm/pip install, DB migrations, .env setup
postStartCommandEvery container startEvery startStart a dev server, apply git config
postAttachCommandWhen a tool attaches to the containerEach attachShow welcome messages, open browser
json — lifecycle script patterns
// String — runs in /bin/sh
"postCreateCommand": "npm install && npm run build",

// Array — executes directly (no shell, preserves quotes)
"postCreateCommand": ["bash", "scripts/setup.sh"],

// Object — runs all commands in PARALLEL
"postCreateCommand": {
  "npm":  "npm install",
  "pip":  "pip install -r requirements.txt",
  "seed": "bash scripts/seed-db.sh"
},

// Recommended: delegate to a script for complex setups
"postCreateCommand": "bash .devcontainer/scripts/post-create.sh"
Networking & Storage

Ports & Mounts

Port Forwarding

forwardPorts makes container ports appear as localhost on your host. More secure than appPort (published ports) because it works with apps that only listen on localhost.

json — port config
"forwardPorts": [3000, 5432, 6379],
"portsAttributes": {
  "3000": {
    "label": "App Server",
    "onAutoForward": "openBrowser"
  },
  "5432": {
    "label": "PostgreSQL",
    "onAutoForward": "silent"
  }
}

onAutoForward options: openBrowser · openBrowserOnce · openPreview · notify · silent · ignore

Mounts

Mount SSH keys, Docker socket, named volumes, or additional host directories using the mounts array.

json — mounts
"mounts": [
  // SSH keys (read-only for safety)
  "source=${localEnv:HOME}/.ssh,\
   target=/home/node/.ssh,\
   type=bind,readonly",

  // Named volume (persists between rebuilds)
  "source=my-app-node_modules,\
   target=/workspaces/app/node_modules,\
   type=volume",

  // Docker socket (Docker-outside-Docker)
  "source=/var/run/docker.sock,\
   target=/var/run/docker.sock,\
   type=bind"
]
Base Configuration

Image vs Dockerfile vs Compose

ApproachUse whenProsCons
image Pre-built image covers all needs; use Features for extras Simplest. Fast start — no build step. MCR images are maintained. Less control over exact image contents
build (Dockerfile) Custom OS packages, specific tool versions, or complex setup Full Dockerfile control. Layer caching. Can share with production. Must maintain Dockerfile. Slightly slower first-time build.
dockerComposeFile Project needs multiple services (DB, cache, queue) during dev Full multi-service parity with production. Services auto-start. Most complex to configure. Compose file to maintain.

Microsoft Base Images (MCR)

ImageTag patternIncludes
mcr.microsoft.com/devcontainers/base:ubuntu :debian :alpineBase OS + common tools (git, curl, zsh, etc.)
mcr.microsoft.com/devcontainers/javascript-node:22 :20Node.js LTS, npm, nvm, yarn
mcr.microsoft.com/devcontainers/python:3.12 :3.11Python, pip, venv, pylint, black
mcr.microsoft.com/devcontainers/dotnet:8.0 :9.0.NET SDK, C# tools
mcr.microsoft.com/devcontainers/go:1.23Go, gopls, delve debugger
mcr.microsoft.com/devcontainers/rust:latestRust toolchain, cargo, rust-analyzer
mcr.microsoft.com/devcontainers/java:21Java JDK, Maven, Gradle
mcr.microsoft.com/devcontainers/universal:linuxNode, Python, Go, Java, .NET, Ruby, PHP — used by Codespaces default
Sample 1 of 5

Sample: Node.js / TypeScript

📦 Node.js 22 + TypeScript + ESLint + Prettier
Full-stack TypeScript project with auto-install on create, port forwarding for the dev server, and a curated extension set. Uses node_modules volume for performance.
json — .devcontainer/devcontainer.json
{
  "name": "Node.js TypeScript",

  // Use MCR Node.js image — maintained, updated regularly
  "image": "mcr.microsoft.com/devcontainers/javascript-node:22",

  // Add the GitHub CLI Feature
  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },

  // Auto-forward dev server and Vite HMR ports
  "forwardPorts": [3000, 5173],
  "portsAttributes": {
    "3000": { "label": "Dev Server", "onAutoForward": "openBrowser" },
    "5173": { "label": "Vite HMR", "onAutoForward": "notify" }
  },

  // node_modules in a volume = much faster I/O on macOS/Windows
  "mounts": [
    "source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
  ],

  // Install deps on container creation
  "postCreateCommand": "npm install",

  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",            // ESLint
        "esbenp.prettier-vscode",            // Prettier
        "ms-vscode.vscode-typescript-next",  // Latest TS
        "bradlc.vscode-tailwindcss",         // Tailwind IntelliSense
        "christian-kohler.path-intellisense",
        "formulahendry.auto-rename-tag"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "editor.codeActionsOnSave": {
          "source.fixAll.eslint": "explicit"
        },
        "typescript.preferences.importModuleSpecifier": "non-relative"
      }
    }
  },

  "remoteUser": "node"
}
Sample 2 of 5

Sample: Python / FastAPI

🐍 Python 3.12 + FastAPI + PostgreSQL + Redis
FastAPI backend with PostgreSQL database and Redis cache — all services running in containers. Uses Docker Compose to coordinate services.
json — .devcontainer/devcontainer.json
{
  "name": "FastAPI Development",

  // Use Compose for multi-service (app + db + redis)
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "runServices": ["db", "redis"],
  "workspaceFolder": "/workspaces/api",
  "shutdownAction": "stopCompose",

  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },

  "forwardPorts": [8000, 5432, 6379],
  "portsAttributes": {
    "8000": { "label": "FastAPI", "onAutoForward": "openBrowser" },
    "5432": { "label": "PostgreSQL", "onAutoForward": "silent" },
    "6379": { "label": "Redis", "onAutoForward": "silent" }
  },

  "containerEnv": {
    "DATABASE_URL": "postgresql+asyncpg://devuser:devpass@db:5432/devdb",
    "REDIS_URL": "redis://redis:6379",
    "PYTHONDONTWRITEBYTECODE": "1"
  },

  // Install deps + run migrations on creation
  "postCreateCommand": {
    "pip":    "pip install -r requirements.txt",
    "hooks": "pre-commit install"
  },
  "postStartCommand": "alembic upgrade head",

  "customizations": {
    "vscode": {
      "extensions": [
        "ms-python.python",
        "ms-python.mypy-type-checker",
        "ms-python.black-formatter",
        "charliermarsh.ruff",
        "tamasfe.even-better-toml",
        "cweijan.vscode-postgresql-client2"
      ],
      "settings": {
        "python.defaultInterpreterPath": "/usr/local/bin/python",
        "editor.formatOnSave": true,
        "[python]": { "editor.defaultFormatter": "ms-python.black-formatter" }
      }
    }
  }
}
yaml — .devcontainer/docker-compose.yml
version: '3.9'
services:
  app:
    build: { context: ., dockerfile: Dockerfile }
    volumes:
      - ../..:/workspaces/api:cached   # project root → container
    command: /bin/sh -c "while sleep 1000; do :; done"
    depends_on: [db, redis]

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: devdb
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  postgres-data:
Sample 3 of 5

Sample: .NET 8 / C#

🟣 .NET 8 ASP.NET Core API + SQL Server
ASP.NET Core API project with SQL Server side-car, hot-reload configured, and OmniSharp/Roslyn tooling pre-installed.
json — .devcontainer/devcontainer.json
{
  "name": ".NET 8 API",

  "build": {
    "dockerfile": "Dockerfile"
  },

  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/node:1": { "version": "20" }  // for tooling
  },

  "forwardPorts": [5000, 5001, 1433],
  "portsAttributes": {
    "5000": { "label": "API (HTTP)",  "onAutoForward": "openBrowser" },
    "5001": { "label": "API (HTTPS)", "onAutoForward": "silent" },
    "1433": { "label": "SQL Server",  "onAutoForward": "silent" }
  },

  "containerEnv": {
    "ASPNETCORE_ENVIRONMENT": "Development",
    "ConnectionStrings__Default": "Server=localhost;Database=DevDb;User=sa;Password=YourStr0ng!Password;TrustServerCertificate=true"
  },

  "postCreateCommand": {
    "restore": "dotnet restore",
    "tools": "dotnet tool restore"
  },
  "postStartCommand": "dotnet dev-certs https --trust 2>/dev/null || true",

  "customizations": {
    "vscode": {
      "extensions": [
        "ms-dotnettools.csharp",
        "ms-dotnettools.csdevkit",
        "ms-dotnettools.dotnet-interactive-vscode",
        "ms-mssql.mssql",
        "humao.rest-client",
        "editorconfig.editorconfig"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "[csharp]": {
          "editor.defaultFormatter": "ms-dotnettools.csharp"
        },
        "dotnet.defaultSolution": "MyApi.sln"
      }
    }
  },

  // Required for dotnet debugging attach
  "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"]
}
dockerfile — .devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/dotnet:8.0

# Install EF Core tools and other global .NET tools
RUN dotnet tool install --global dotnet-ef \
 && dotnet tool install --global dotnet-outdated-tool \
 && dotnet tool install --global csharpier

# Ensure tools are on PATH
ENV PATH="${PATH}:/root/.dotnet/tools"
Sample 4 of 5

Sample: Multi-Service Stack

🔀 Node.js API + React SPA + PostgreSQL + Redis + Nginx
Full-stack monorepo with separate backend and frontend services, shared services, and Nginx as a local reverse proxy. Two dev containers — one per service.
yaml — .devcontainer/docker-compose.yml
version: '3.9'
services:

  # ── Backend API (VS Code connects here) ────────────
  api:
    build: { context: ../../api, dockerfile: .devcontainer/Dockerfile }
    volumes: ["../..:/workspaces:cached"]
    command: sleep infinity
    environment:
      NODE_ENV: development
      DATABASE_URL: postgresql://dev:dev@db:5432/appdb
      REDIS_URL: redis://redis:6379
    depends_on: [db, redis]
    ports: ["3001:3001"]

  # ── Frontend SPA ───────────────────────────────────
  web:
    build: { context: ../../web, dockerfile: .devcontainer/Dockerfile }
    volumes: ["../..:/workspaces:cached"]
    command: sleep infinity
    ports: ["5173:5173"]

  # ── Shared services ────────────────────────────────
  db:
    image: postgres:16
    environment: { POSTGRES_USER: dev, POSTGRES_PASSWORD: dev, POSTGRES_DB: appdb }
    volumes: ["pgdata:/var/lib/postgresql/data"]

  redis:
    image: redis:7-alpine

  nginx:
    image: nginx:alpine
    volumes: ["./nginx.conf:/etc/nginx/nginx.conf"]
    ports: ["80:80"]
    depends_on: [api, web]

volumes:
  pgdata:
json — api/.devcontainer/devcontainer.json
{
  "name": "API Service",
  "dockerComposeFile": "../../.devcontainer/docker-compose.yml",
  "service": "api",
  "runServices": ["db", "redis", "nginx"],
  "workspaceFolder": "/workspaces/api",
  "forwardPorts": [3001, 5432, 6379, 80],
  "postCreateCommand": "npm install",
  "customizations": {
    "vscode": {
      "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode",
                      "cweijan.vscode-postgresql-client2"]
    }
  }
}
Sample 5 of 5

Sample: GitHub Codespaces

☁️ Codespaces-Optimised with Prebuilds
The same devcontainer.json works in Codespaces. Additional fields like hostRequirements, Codespaces-specific settings, and prebuild strategy are used for cloud optimization.
json — optimised for GitHub Codespaces
{
  "name": "My App (Codespaces)",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:22",

  // Minimum hardware for this project (Codespaces uses this for machine sizing)
  "hostRequirements": {
    "cpus": 4,
    "memory": "8gb",
    "storage": "32gb"
  },

  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },

  "forwardPorts": [3000, 5000],

  // onCreateCommand runs during prebuild — heavy work done before you open
  "onCreateCommand": "npm install",
  // updateContentCommand runs when source changes during prebuild
  "updateContentCommand": "npm install",
  // postCreateCommand runs AFTER prebuild merge — after you open the Codespace
  "postCreateCommand": "npm run db:migrate",

  "customizations": {
    "vscode": {
      "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
    },
    // Codespaces-specific: GitHub Copilot integration
    "codespaces": {
      "openFiles": ["README.md", "src/index.ts"]
    }
  }
}
💡
Codespaces Prebuilds: Enable prebuilds in your GitHub repo settings to have Codespaces pre-run onCreateCommand and updateContentCommand on a schedule. This means opening a Codespace takes seconds instead of minutes — deps are already installed when the image is ready.
Tooling

Dev Container CLI

The devcontainer CLI is the official command-line tool for building and running dev containers outside of VS Code — perfect for CI pipelines, scripting, and server-side use.

bash — install and common commands
# Install globally via npm
npm install -g @devcontainers/cli

# Build and open a project in a container
devcontainer up --workspace-folder ./my-project

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

# Build the container image (without starting)
devcontainer build --workspace-folder . --image-name myapp:dev

# Run feature tests
devcontainer features test --workspace-folder . --features ./my-feature

# Rebuild from scratch (no cache)
devcontainer up --workspace-folder . --remove-existing-container

Using Dev Containers in CI (GitHub Actions)

yaml — github actions using devcontainer/ci
name: CI in Dev Container

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6

      # Build and run commands inside the dev container
      - name: Build and test in dev container
        uses: devcontainers/ci@v0.3
        with:
          imageName: ghcr.io/my-org/my-app-devcontainer
          cacheFrom: ghcr.io/my-org/my-app-devcontainer
          push: filter                # push image on main branch only
          refFilterForPush: refs/heads/main
          runCmd: |
            npm run lint
            npm test
            npm run build
Best Practices

Tips & Tricks

TipDetail
Use named volumes for node_modules On macOS/Windows, bind mounts to node_modules are very slow. Mount a named Docker volume over it: source=proj-node_modules,target=.../node_modules,type=volume
Pin image tags, not :latest Use :22-bookworm not :latest. Floating tags can break your environment on the next pull. Use Dependabot for the docker ecosystem to update automatically.
SSH keys via bind mount Mount your ~/.ssh directory as readonly: "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly". Never bake SSH keys into images.
Share git config Mount ~/.gitconfig from the host: "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly"
Use postCreateCommand for deps Run npm install / pip install in postCreateCommand, not in the Dockerfile. This ensures deps track your lockfile and are re-installed when the lockfile changes.
Parallel postCreateCommand Use the object form of lifecycle commands to run independent tasks in parallel: { "npm": "npm install", "pip": "pip install -r requirements.txt" }
Delegate to a setup script For complex setup, write .devcontainer/scripts/post-create.sh and call it. Easier to maintain, testable, and readable vs a long inline command string.
Rebuild vs Reopen "Reopen in Container" reuses the existing container. "Rebuild Container" builds a fresh one — use this after changing the Dockerfile or Features. "Rebuild Without Cache" forces full rebuild.
Multiple configs for a mono-repo Create .devcontainer/frontend/devcontainer.json and .devcontainer/backend/devcontainer.json. VS Code will prompt you to choose at open time.
Secrets in containers Never put secrets in containerEnv or the Dockerfile. Use remoteEnv to pull from host environment variables, or mount a .env file via postCreateCommand.
What to Avoid

Anti-Patterns & Pitfalls

Anti-patternProblemFix
npm install in Dockerfile Dependencies cached in the image — don't update when lockfile changes. Slow to rebuild. Run npm install in postCreateCommand instead. Image just provides Node.
Using :latest tags Container silently breaks when the upstream image changes on next pull or rebuild. Pin to a specific version tag. Use Dependabot docker ecosystem to keep it updated.
Secrets in containerEnv containerEnv is stored in image metadata — visible in Compose files, devcontainer.json, CI logs. Use remoteEnv with ${localEnv:MY_SECRET} or mount a gitignored .env file.
Everything in one huge Dockerfile Hard to maintain. Changes to one tool invalidate all layer cache. Slow rebuilds. Use Features for standard tooling. Only put project-specific layers in the Dockerfile.
Skipping forwardPorts Services only listen on localhost inside the container — not reachable from host browser. Declare all dev server ports in forwardPorts. They'll be accessible on host localhost automatically.
Running as root Files created in the container are owned by root — permission issues on host files. Set "remoteUser": "vscode" (or appropriate non-root user). Set updateRemoteUserUID: true on Linux.
Giant monolithic devcontainer.json Hard to review, hard to understand, slows onboarding documentation value. Split complex setups into named configs (.devcontainer/backend/, .devcontainer/frontend/).
Not committing devcontainer.json The container config exists only locally — no team benefit, defeats the purpose. Always commit .devcontainer/ to git. It is team infrastructure, not a personal preference file.
Review

Dev Container Checklist

Click each item to track your configuration review.

  • devcontainer.json committed — in .devcontainer/ and tracked by version control.
  • Image tag pinned — not :latest. Using a specific version like :22 or :3.12-bookworm.
  • Dependency install in lifecycle hooknpm install / pip install in postCreateCommand, not in the Dockerfile.
  • No secrets in containerEnv — using remoteEnv with ${localEnv:} substitution or mounted .env files.
  • forwardPorts configured — all dev server ports declared so they're accessible from the host browser.
  • remoteUser set — non-root user specified. updateRemoteUserUID: true set for Linux hosts.
  • Extensions declared — team-standard VS Code extensions in customizations.vscode.extensions.
  • Editor settings enforced — format-on-save, default formatter, and language settings in customizations.vscode.settings.
  • node_modules volume (if Node project) — named volume over node_modules for macOS/Windows performance.
  • SSH keys mounted (if needed) — readonly bind mount of ~/.ssh, not baked into image.
  • Dockerfile minimal (if using one) — only project-specific layers; use Features for standard tooling.
  • Tested on a clean machine — another team member has opened the container from scratch and confirmed it works.
Dev Containers — Complete Handbook 2025–2026 containers.dev · VS Code · GitHub Codespaces