Field Guide
Dev Containers
DOC-DEVC-2026 · v1
Developer Environment Handbook · June 2026

Dev
Containers

// "Works on my machine" — solved. Ship the machine.

The complete guide to Dev Containers: how they eliminate environment drift, how to configure them for Python, React, full-stack apps, and data science workflows, and how to make them production-grade with lifecycle scripts, secrets, and non-root users.

VS Code Docker Python React / TypeScript Docker Compose GitHub Codespaces
01

What Are Dev Containers

// DEVELOPMENT ENVIRONMENT AS CODE

A Dev Container is a Docker container configured specifically for development — not production. It packages the runtime, dependencies, tools, compilers, linters, debuggers, and VS Code extensions your project needs into a reproducible, version-controlled environment defined entirely in code.

The VS Code Dev Containers extension (part of the Remote Development Pack) reads a .devcontainer/devcontainer.json file in your repository, builds or pulls the specified container, mounts your workspace into it, and connects VS Code to the container over a local socket — so you get full IntelliSense, debugging, and terminal access as if everything were installed locally.

Reproducible

Every developer, CI runner, and GitHub Codespace gets an identical environment. No more "works on my machine" — you define the machine.

Isolated

Your Python 3.11 FastAPI project and your Python 3.9 legacy service live in separate containers. Zero conflicts, no virtual-env juggling, no nvm headaches.

Version-Controlled

The entire dev environment is defined in .devcontainer/devcontainer.json — committed to the repo, reviewed in PRs, and evolved with the codebase.

Dev Containers ≠ Production Containers. A dev container is optimised for the development experience — it includes development tools, bind-mounts your source code, and may run as root. Your production Dockerfile is separate, optimised for minimal size and security.
02

Why Use Dev Containers

// THE PROBLEMS THEY SOLVE
✕ Without Dev Containers
  • "Works on my machine" — broken onboarding
  • 3-hour setup docs that go stale in a week
  • Conflicting Python / Node / Ruby versions across projects
  • System-level dependencies installed globally and forgotten
  • New team member writes their first PR on day 3, not day 1
  • CI fails because CI has a different Node version
  • Windows vs macOS vs Linux path differences
✓ With Dev Containers
  • git cloneReopen in Container → start coding
  • Environment definition is the setup doc — always current
  • Each repo carries its exact runtime version
  • All tools installed automatically on container build
  • Productive on day 1 — same experience as the team lead
  • CI uses the same devcontainer image — parity guaranteed
  • Linux container, same everywhere

Key Use Cases

Team Onboarding High Value

New engineer clones the repo, VS Code prompts "Reopen in Container", and within 2 minutes they have a fully working dev environment. No install docs to follow, no dependency conflicts to debug.

Multiple Projects Isolation

Python 3.9 Django app, Python 3.12 ML pipeline, and a Node 22 API — all on the same laptop, all in separate containers, zero version manager gymnastics.

Consistent CI Parity

Run devcontainer build in CI to produce the exact same environment your developers use. Tests that fail locally fail in CI. Tests that pass locally pass in CI.

Open Source Contributions Accessibility

Contributors fork and get a working environment instantly — no risk of their local setup conflicting with the project. Lower barrier to contribution.

03

How Dev Containers Work

// THE LIFECYCLE FROM FILE TO RUNNING ENVIRONMENT

When you open a folder in VS Code and select "Reopen in Container", the Dev Containers extension reads .devcontainer/devcontainer.json, builds or pulls the container image, runs lifecycle scripts, and connects VS Code to the running container. Your source code is bind-mounted from your host into the container at /workspaces/<repo-name>.

📂
Read
devcontainer .json
🏗️
Build
Docker Image
🚀
Start
Container
⚙️
Run
Lifecycle Hooks
🔌
Connect
VS Code Server
💻
Code
Full IntelliSense

Three Ways to Define a Container

ApproachConfigBest ForTrade-off
Pre-built image "image": "mcr.microsoft.com/devcontainers/python:3.12" Quick start, standard stacks Less control over exact environment
Custom Dockerfile "build": { "dockerfile": "Dockerfile" } Full control, custom tools Longer initial build, maintain Dockerfile
Docker Compose "dockerComposeFile": "docker-compose.yml" Multiple services (DB, Redis, API) More config, more power
Source code is bind-mounted, not copied. When you edit a file in VS Code, you are editing the file on your host. The container sees it immediately via the bind mount. This means your changes persist even if you rebuild the container, and you can use your host Git credentials and SSH keys normally.
04

When to Use Dev Containers

// DECISION GUIDE
SituationUse Dev Container?Reason
Team project with shared toolchain ✓ Yes Every dev gets identical tools — eliminates "works on my machine"
Open-source project with contributors ✓ Yes Zero-setup contributor experience; works with GitHub Codespaces
Project with system-level dependencies (libpq, ffmpeg, etc.) ✓ Yes Install in Dockerfile once; no per-developer setup required
Multiple projects needing conflicting runtimes ✓ Yes Container-level isolation eliminates conflicts entirely
Quick personal script in a familiar language ⚠ Maybe Overhead may not be worth it for a 50-line script you won't share
Project already using nix or mise for env management ⚠ Maybe Consider whether you need both; dev containers work alongside these tools
Performance-critical local work on Apple Silicon (M-series) ⚠ Consider Docker on macOS has I/O overhead; use cached mounts; often fine for code
05

Prerequisites & Installation

// WHAT YOU NEED BEFORE YOU START
1 · Docker Required
  • macOS / Windows: Docker Desktop (includes Docker Engine + CLI)
  • Linux: Docker Engine + Docker CLI (no Docker Desktop needed)
  • Ensure Docker daemon is running before opening VS Code
  • Minimum: Docker 20.10+
2 · VS Code Required
  • VS Code 1.75+ (any recent version works)
  • Install the Dev Containers extension (ms-vscode-remote.remote-containers)
  • Or install the Remote Development extension pack which includes it
3 · Dev Container CLI Optional
  • For CI usage and testing without VS Code
  • npm install -g @devcontainers/cli
  • Enables devcontainer build, devcontainer up, devcontainer exec
bash — install CLI and verify
# Install the Dev Containers CLI (optional, for CI usage) npm install -g @devcontainers/cli # Verify Docker is running docker info # Build a dev container image from the CLI (no VS Code needed) devcontainer build --workspace-folder . # Start the container and run a command inside it devcontainer up --workspace-folder . devcontainer exec --workspace-folder . npm test
macOS performance tip: In Docker Desktop → Settings → Resources → Advanced, enable VirtioFS (macOS 12.5+) as the file sharing implementation. It is significantly faster than the legacy gRPC FUSE implementation for bind mounts.
06

Folder Structure

// WHERE FILES LIVE IN YOUR REPO

The Dev Containers extension looks for configuration in the .devcontainer/ directory at the root of your workspace. A minimal setup requires only devcontainer.json. As complexity grows, you add a Dockerfile, scripts, and optional Docker Compose files.

my-project/ ├── .devcontainer/ # all devcontainer config lives here │ ├── devcontainer.json # main config — required │ ├── Dockerfile # custom image — optional │ ├── docker-compose.yml # multi-service — optional │ └── scripts/ # lifecycle scripts — optional │ ├── postCreate.sh # runs after container creation │ └── onStartCommand.sh # runs every container start ├── src/ ├── .gitignore └── README.md
Multiple dev containers in one repo: Place configurations in .devcontainer/<name>/devcontainer.json. VS Code lets you choose which one to open when you run "Reopen in Container". Useful for repos that have both a backend and a frontend with different runtimes.
monorepo/ └── .devcontainer/ ├── backend/ │ └── devcontainer.json # Python 3.12 + FastAPI tools └── frontend/ └── devcontainer.json # Node 22 + React tools
07

devcontainer.json Anatomy

// EVERY IMPORTANT FIELD EXPLAINED

The devcontainer.json file is the contract between your repository and the development environment. It controls the image or Dockerfile used, what features are installed, VS Code extensions, port forwarding, lifecycle hooks, and environment variables.

json — annotated devcontainer.json (all options)
{ // ─── Identity ─────────────────────────────────────────────── "name": "My Project Dev Container", // ─── Image source (pick ONE of: image, build, dockerComposeFile) "image": "mcr.microsoft.com/devcontainers/python:3.12", // OR build from a local Dockerfile: "build": { "dockerfile": "Dockerfile", "context": ".", "args": { "VARIANT": "3.12" } }, // OR use Docker Compose for multi-service setups: "dockerComposeFile": "docker-compose.yml", "service": "app", "workspaceFolder": "/workspaces/my-project", // ─── Dev Container Features ───────────────────────────────── // Features install tools ON TOP of the base image "features": { "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/devcontainers/features/node:1": { "version": "22" }, "ghcr.io/devcontainers/features/docker-in-docker:2": {} }, // ─── Port Forwarding ──────────────────────────────────────── "forwardPorts": [3000, 8000, 5432], "portsAttributes": { "3000": { "label": "Frontend", "onAutoForward": "notify" }, "8000": { "label": "API Server", "onAutoForward": "openPreview" } }, // ─── VS Code Customisation ────────────────────────────────── "customizations": { "vscode": { "extensions": [ "ms-python.python", "ms-python.vscode-pylance", "charliermarsh.ruff", "ms-python.debugpy" ], "settings": { "python.defaultInterpreterPath": "/usr/local/bin/python", "editor.formatOnSave": true, "editor.defaultFormatter": "charliermarsh.ruff" } } }, // ─── Lifecycle Scripts ────────────────────────────────────── "initializeCommand": "echo 'Runs on HOST before container starts'", "onCreateCommand": "pip install -r requirements.txt", "updateContentCommand": "pip install -r requirements.txt", "postCreateCommand": "bash .devcontainer/scripts/postCreate.sh", "postStartCommand": "echo 'Container started at $(date)'", "postAttachCommand": "echo 'VS Code attached'", // ─── Container Configuration ──────────────────────────────── "remoteUser": "vscode", "remoteEnv": { "PYTHONPATH": "${containerWorkspaceFolder}/src", "DATABASE_URL": "postgresql://user:pass@db:5432/devdb" }, // ─── Mounts ───────────────────────────────────────────────── "mounts": [ "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly", "source=project-venv,target=/workspaces/my-project/.venv,type=volume" ], // ─── Run Arguments ────────────────────────────────────────── "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"], "overrideCommand": false, // ─── Shut down behaviour ──────────────────────────────────── "shutdownAction": "stopContainer" }

Core Fields Quick Reference

FieldTypeWhat it does
namestringDisplay name shown in VS Code status bar
imagestringDocker image to use directly (no Dockerfile needed)
buildobjectBuild from a local Dockerfile; supports args and target
dockerComposeFilestring/arrayPath to Docker Compose file(s); requires service
featuresobjectInstall Dev Container Features (tools) on top of any base image
forwardPortsarrayPorts to forward from container to host automatically
customizations.vscode.extensionsarrayVS Code extension IDs installed inside the container
customizations.vscode.settingsobjectVS Code settings applied inside the container
postCreateCommandstring/arrayRuns once after container is created (install deps, seed db, etc.)
remoteUserstringUser VS Code runs as inside the container (use non-root for security)
remoteEnvobjectEnvironment variables set for VS Code and terminal sessions
mountsarrayAdditional bind mounts or named volumes
runArgsarrayExtra docker run arguments (capabilities, security opts)
08

Python Dev Container

// FASTAPI / DJANGO / FLASK WITH FULL TOOLCHAIN

A Python dev container should provide the correct Python version, a virtual environment inside a named volume (so rebuilding the container doesn't reinstall packages), linting with Ruff, formatting, debugging with debugpy, and database access if needed.

Minimal Python Container (Image-Based)

json — .devcontainer/devcontainer.json
{ "name": "Python 3.12", // Microsoft's official Python devcontainer image "image": "mcr.microsoft.com/devcontainers/python:3.12-bullseye", "features": { "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {} }, "forwardPorts": [8000], "portsAttributes": { "8000": { "label": "FastAPI / Django", "onAutoForward": "openPreview" } }, "customizations": { "vscode": { "extensions": [ "ms-python.python", "ms-python.vscode-pylance", "ms-python.debugpy", "charliermarsh.ruff", "ms-python.mypy-type-checker", "tamasfe.even-better-toml", "redhat.vscode-yaml" ], "settings": { "python.defaultInterpreterPath": "/workspaces/my-project/.venv/bin/python", "editor.formatOnSave": true, "editor.defaultFormatter": "charliermarsh.ruff", "[python]": { "editor.codeActionsOnSave": { "source.fixAll": "explicit" } } } } }, // Install dependencies after container creation "postCreateCommand": "pip install uv && uv venv .venv && uv pip install -r requirements.txt", "remoteEnv": { "PYTHONPATH": "${containerWorkspaceFolder}/src" }, // Keep .venv in a named volume — survives container rebuilds "mounts": [ "source=python-venv,target=/workspaces/my-project/.venv,type=volume" ], "remoteUser": "vscode" }

Production-Grade Python Container (Custom Dockerfile)

dockerfile — .devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/python:3.12-bullseye # Install system dependencies your project needs RUN apt-get update && apt-get install -y --no-install-recommends \ libpq-dev \ # PostgreSQL client headers (for psycopg2) libssl-dev \ curl \ jq \ && rm -rf /var/lib/apt/lists/* # Install uv — the fast Python package manager RUN pip install --no-cache-dir uv # Install global dev tools RUN pip install --no-cache-dir \ ruff \ mypy \ pytest \ pre-commit # Switch to non-root user (already exists in the base image) USER vscode
json — .devcontainer/devcontainer.json (with Dockerfile)
{ "name": "Python 3.12 — FastAPI", "build": { "dockerfile": "Dockerfile", "context": "." }, "forwardPorts": [8000], "customizations": { "vscode": { "extensions": [ "ms-python.python", "ms-python.vscode-pylance", "ms-python.debugpy", "charliermarsh.ruff", "ms-python.mypy-type-checker" ], "settings": { "python.defaultInterpreterPath": "/workspaces/${localWorkspaceFolderBasename}/.venv/bin/python", "python.terminal.activateEnvironment": true, "editor.formatOnSave": true } } }, "postCreateCommand": "bash .devcontainer/scripts/postCreate.sh", "mounts": [ "source=fastapi-venv,target=/workspaces/${localWorkspaceFolderBasename}/.venv,type=volume" ], "remoteUser": "vscode" }
bash — .devcontainer/scripts/postCreate.sh
#!/bin/bash set -e echo "==> Setting up Python virtual environment..." uv venv .venv echo "==> Installing dependencies..." uv pip install -r requirements.txt if [ -f requirements-dev.txt ]; then uv pip install -r requirements-dev.txt fi echo "==> Installing pre-commit hooks..." pre-commit install echo "==> Dev container ready!"

VS Code launch.json for FastAPI Debugging

json — .vscode/launch.json
{ "version": "0.2.0", "configurations": [ { "name": "FastAPI: Debug", "type": "debugpy", "request": "launch", "module": "uvicorn", "args": ["src.main:app", "--reload", "--port", "8000"], "jinja": true, "justMyCode": true }, { "name": "Django: Run Server", "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/manage.py", "args": ["runserver", "0.0.0.0:8000"], "django": true, "justMyCode": true } ] }
09

React / TypeScript Dev Container

// VITE · NEXT.JS · CREATE REACT APP

A React / TypeScript dev container needs the right Node.js version, a fast package manager, hot-reload port forwarding, ESLint, Prettier, and optionally a browser preview. Node modules go in a named volume so rebuilding the container doesn't wipe node_modules.

json — .devcontainer/devcontainer.json (React / Vite)
{ "name": "React TypeScript — Vite", // Official Node devcontainer image — pin to a specific LTS "image": "mcr.microsoft.com/devcontainers/typescript-node:22-bookworm", "features": { "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {} }, // Vite dev server (5173) + Storybook (6006) + test UI (51204) "forwardPorts": [5173, 3000, 6006], "portsAttributes": { "5173": { "label": "Vite Dev Server", "onAutoForward": "openPreview" }, "3000": { "label": "Next.js / CRA", "onAutoForward": "openPreview" }, "6006": { "label": "Storybook", "onAutoForward": "notify" } }, "customizations": { "vscode": { "extensions": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss", "dsznajder.es7-react-js-snippets", "styled-components.vscode-styled-components", "ms-vscode.vscode-typescript-next", "orta.vscode-jest", "vitest.explorer" ], "settings": { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "typescript.tsdk": "node_modules/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true } } }, // Install dependencies after container creation "postCreateCommand": "npm ci", // node_modules in a named volume — rebuilds don't wipe dependencies "mounts": [ "source=react-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume" ], "remoteUser": "node" }

Custom Dockerfile for React (with pnpm and extra tools)

dockerfile — .devcontainer/Dockerfile (React + pnpm)
FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm # Install pnpm globally RUN npm install -g pnpm@9 # Install Playwright system dependencies for E2E testing RUN apt-get update && apt-get install -y --no-install-recommends \ libgbm-dev libasound2 \ && rm -rf /var/lib/apt/lists/* # Install useful global tools RUN npm install -g \ vite@latest \ @playwright/test \ serve USER node
json — .devcontainer/devcontainer.json (Next.js)
{ "name": "Next.js 15 TypeScript", "build": { "dockerfile": "Dockerfile" }, "forwardPorts": [3000], "portsAttributes": { "3000": { "label": "Next.js", "onAutoForward": "openPreview" } }, "customizations": { "vscode": { "extensions": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss", "ms-vscode.vscode-typescript-next", "ms-playwright.playwright" ], "settings": { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" } } }, "postCreateCommand": "pnpm install", "mounts": [ "source=nextjs-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume", "source=nextjs-next-cache,target=${containerWorkspaceFolder}/.next,type=volume" ], "remoteUser": "node" }
Hot reload inside a container: Vite requires server.host: '0.0.0.0' in vite.config.ts to be reachable through the forwarded port. Next.js requires --hostname 0.0.0.0 in the dev script. Without this, the server binds to localhost inside the container and port forwarding cannot reach it.
typescript — vite.config.ts (required for port forwarding)
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { host: '0.0.0.0', // ← required — bind to all interfaces port: 5173, watch: { usePolling: true, // ← may be needed for hot reload in Docker on macOS } } })
10

Full-Stack with Docker Compose

// API + POSTGRES + REDIS + FRONTEND

When your project needs multiple services — a database, a cache, a background worker — Docker Compose is the right tool. The Dev Containers extension can start the full Compose stack and connect VS Code to one specific service (your app container) while the others run alongside it.

fullstack-app/ ├── .devcontainer/ │ ├── devcontainer.json │ ├── docker-compose.yml │ └── Dockerfile # dev Dockerfile for the app service ├── backend/ # FastAPI or Django code ├── frontend/ # React / Next.js code └── Makefile
yaml — .devcontainer/docker-compose.yml
version: "3.9" services: # ── App (VS Code connects to this service) ───────────────── app: build: context: . dockerfile: Dockerfile volumes: # Bind-mount source code - ..:/workspaces/fullstack-app:cached # Named volume for Python venv (survives rebuilds) - app-venv:/workspaces/fullstack-app/.venv environment: - DATABASE_URL=postgresql://devuser:devpass@db:5432/devdb - REDIS_URL=redis://redis:6379/0 - SECRET_KEY=not-a-secret-in-dev ports: - "8000:8000" depends_on: - db - redis # Keep container running — VS Code dev containers need this command: sleep infinity networks: - devnet # ── PostgreSQL ────────────────────────────────────────────── db: image: postgres:16-alpine restart: unless-stopped volumes: - postgres-data:/var/lib/postgresql/data environment: - POSTGRES_USER=devuser - POSTGRES_PASSWORD=devpass - POSTGRES_DB=devdb ports: - "5432:5432" # forward so you can connect from host with pgAdmin networks: - devnet # ── Redis ─────────────────────────────────────────────────── redis: image: redis:7-alpine restart: unless-stopped ports: - "6379:6379" networks: - devnet # ── Mailpit (local email catcher) ─────────────────────────── mailpit: image: axllent/mailpit:latest ports: - "8025:8025" # web UI - "1025:1025" # SMTP networks: - devnet volumes: postgres-data: app-venv: networks: devnet:
json — .devcontainer/devcontainer.json (Compose)
{ "name": "Full Stack — FastAPI + React + Postgres", "dockerComposeFile": "docker-compose.yml", "service": "app", // VS Code attaches to this service "workspaceFolder": "/workspaces/fullstack-app", // These ports are forwarded from the compose network to your host "forwardPorts": [8000, 3000, 5432, 6379, 8025], "portsAttributes": { "8000": { "label": "FastAPI" }, "3000": { "label": "React" }, "5432": { "label": "Postgres" }, "8025": { "label": "Mailpit UI"} }, "customizations": { "vscode": { "extensions": [ "ms-python.python", "ms-python.debugpy", "charliermarsh.ruff", "mtxr.sqltools", "mtxr.sqltools-driver-pg", "cweijan.vscode-redis-client" ] } }, "postCreateCommand": "bash .devcontainer/scripts/postCreate.sh", // Shut down the entire Compose stack when VS Code closes "shutdownAction": "stopCompose", "remoteUser": "vscode" }
bash — .devcontainer/scripts/postCreate.sh (Full Stack)
#!/bin/bash set -e WORKDIR=/workspaces/fullstack-app echo "==> Installing backend Python dependencies..." cd $WORKDIR/backend uv venv ../.venv uv pip install -r requirements.txt -r requirements-dev.txt echo "==> Waiting for Postgres to be ready..." until pg_isready -h db -U devuser -d devdb; do echo " postgres is not ready yet..." sleep 2 done echo " postgres ready!" echo "==> Running DB migrations..." cd $WORKDIR/backend .venv/bin/alembic upgrade head # or: python manage.py migrate echo "==> Installing frontend Node dependencies..." cd $WORKDIR/frontend npm ci echo "==> All done — happy coding!"
11

Data Science Dev Container

// JUPYTER · PANDAS · SCIKIT-LEARN · PYTORCH

Data science containers need Jupyter, the scientific Python stack, proper kernel registration, and often GPU access. The Microsoft universal image is the easiest starting point — it bundles conda, common data science libraries, and Jupyter support out of the box.

dockerfile — .devcontainer/Dockerfile (Data Science)
FROM mcr.microsoft.com/devcontainers/python:3.12-bullseye # Install system packages needed for data science RUN apt-get update && apt-get install -y --no-install-recommends \ libgomp1 \ # OpenMP for parallel processing libhdf5-dev \ # HDF5 for h5py / large datasets libgeos-dev \ # for geospatial libraries (shapely) graphviz \ # for graph visualisation && rm -rf /var/lib/apt/lists/* # Install uv for fast dependency management RUN pip install --no-cache-dir uv # Install core scientific stack globally (for Jupyter kernel) RUN pip install --no-cache-dir \ jupyterlab==4.* \ ipywidgets \ notebook USER vscode
json — .devcontainer/devcontainer.json (Data Science)
{ "name": "Data Science — Python 3.12", "build": { "dockerfile": "Dockerfile" }, // Jupyter Lab (8888) and optional Tensorboard (6006) "forwardPorts": [8888, 6006], "portsAttributes": { "8888": { "label": "Jupyter Lab", "onAutoForward": "openPreview" }, "6006": { "label": "TensorBoard", "onAutoForward": "notify" } }, "customizations": { "vscode": { "extensions": [ "ms-toolsai.jupyter", // Jupyter notebook support "ms-toolsai.jupyter-keymap", "ms-toolsai.vscode-jupyter-slideshow", "ms-python.python", "ms-python.vscode-pylance", "ms-python.debugpy", "charliermarsh.ruff", "randomfractalsinc.vscode-data-preview" ], "settings": { "python.defaultInterpreterPath": "/workspaces/${localWorkspaceFolderBasename}/.venv/bin/python", "jupyter.kernels.trusted": ["*"], "notebook.formatOnSave.enabled": true } } }, "postCreateCommand": "bash .devcontainer/scripts/postCreate.sh", "mounts": [ "source=ds-venv,target=/workspaces/${localWorkspaceFolderBasename}/.venv,type=volume" ], "remoteUser": "vscode" }
bash — .devcontainer/scripts/postCreate.sh (Data Science)
#!/bin/bash set -e echo "==> Creating virtual environment..." uv venv .venv echo "==> Installing data science packages..." uv pip install \ numpy pandas scipy matplotlib seaborn \ scikit-learn xgboost lightgbm \ torch torchvision \ # or: tensorflow plotly ipykernel ipywidgets \ jupyter jupyterlab \ dask[dataframe] \ mlflow optuna echo "==> Registering Jupyter kernel..." .venv/bin/python -m ipykernel install --user --name=project-venv --display-name="Project (.venv)" echo "==> Setup complete. Open a .ipynb file or run 'jupyter lab' in the terminal."
12

Node.js API Dev Container

// EXPRESS · FASTIFY · NestJS WITH MONGODB OR POSTGRES
json — .devcontainer/devcontainer.json (NestJS + MongoDB)
{ "name": "NestJS API", "image": "mcr.microsoft.com/devcontainers/typescript-node:22-bookworm", "features": { "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {} }, "forwardPorts": [3000, 27017, 8081], "portsAttributes": { "3000": { "label": "NestJS API" }, "27017": { "label": "MongoDB" }, "8081": { "label": "Mongo Express UI" } }, "customizations": { "vscode": { "extensions": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "ms-vscode.vscode-typescript-next", "mongodb.mongodb-vscode", "hbenl.vscode-test-explorer", "pkief.material-icon-theme" ], "settings": { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode" } } }, "postCreateCommand": "npm ci && npm run build", "mounts": [ "source=nestjs-modules,target=${containerWorkspaceFolder}/node_modules,type=volume" ], "remoteUser": "node" }
yaml — docker-compose.yml (NestJS + MongoDB)
services: app: image: mcr.microsoft.com/devcontainers/typescript-node:22-bookworm volumes: - ..:/workspaces/nestjs-api:cached - nestjs-modules:/workspaces/nestjs-api/node_modules environment: - MONGODB_URI=mongodb://mongo:27017/devdb command: sleep infinity depends_on: [mongo] mongo: image: mongo:7 volumes: [mongo-data:/data/db] ports: ["27017:27017"] mongo-express: image: mongo-express:1.0 environment: - ME_CONFIG_MONGODB_SERVER=mongo - ME_CONFIG_BASICAUTH=false ports: ["8081:8081"] depends_on: [mongo] volumes: mongo-data: nestjs-modules:
13

Dev Container Features

// COMPOSABLE TOOL INSTALLERS — NO DOCKERFILE NEEDED

Dev Container Features are self-contained units that install tools into any base image. They run before postCreateCommand and work with any image — you don't need to maintain a Dockerfile just to add git, Node.js, or the AWS CLI to a Python container.

json — common features in devcontainer.json
{ "features": { // ── Languages & Runtimes ────────────────────────────────── "ghcr.io/devcontainers/features/node:1": { "version": "22" }, "ghcr.io/devcontainers/features/python:1": { "version": "3.12" }, "ghcr.io/devcontainers/features/go:1": { "version": "1.22" }, "ghcr.io/devcontainers/features/rust:1": { "version": "latest" }, // ── Version Control ────────────────────────────────────── "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/git-lfs:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/devcontainers/features/gitlab-cli:1": {}, // ── Cloud & Infrastructure ─────────────────────────────── "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { "version": "1.30", "helm": "3.14" }, "ghcr.io/devcontainers/features/aws-cli:1": {}, "ghcr.io/devcontainers/features/terraform:1": { "version": "latest" }, "ghcr.io/devcontainers/features/azure-cli:1": {}, // ── Databases & CLI Tools ──────────────────────────────── "ghcr.io/devcontainers/features/sshd:1": {}, "ghcr.io/devcontainers/features/common-utils:2": { "zsh": true }, // ── Docker-in-Docker (build containers inside a container) "ghcr.io/devcontainers/features/docker-in-docker:2": { "version": "latest", "dockerDashComposeVersion": "v2" } } }
Browse all available features at containers.dev/features — the official registry maintained by Microsoft and the community. Features are versioned, open-source, and composable: add as many as your project needs without modifying a Dockerfile.
FeatureOCI ReferenceUse Case
Gitghcr.io/devcontainers/features/git:1Latest git, always
GitHub CLIghcr.io/devcontainers/features/github-cli:1PR, release management in terminal
Node.jsghcr.io/devcontainers/features/node:1Add Node to any non-Node image
Docker-in-Dockerghcr.io/devcontainers/features/docker-in-docker:2Build images, run Compose inside the container
kubectl + Helmghcr.io/devcontainers/features/kubectl-helm-minikube:1Kubernetes development
AWS CLIghcr.io/devcontainers/features/aws-cli:1AWS deployments from the container
Terraformghcr.io/devcontainers/features/terraform:1IaC work alongside your app
common-utilsghcr.io/devcontainers/features/common-utils:2zsh, oh-my-zsh, useful CLI tools
14

Lifecycle Scripts

// HOOKS THAT RUN AT EACH STAGE OF THE CONTAINER LIFECYCLE

Dev Containers defines six lifecycle hooks. Each runs at a different phase — some only once (when the image is first created), some every time VS Code starts the container. Knowing which to use prevents you from running slow dependency installs on every restart.

HookRuns WhenWhereBest For
initializeCommand Before container starts Host machine Validate Docker is running, pull secrets from host keychain
onCreateCommand Once — when container is first created Container First-time setup that should never repeat (initial clone, etc.)
updateContentCommand After clone/update; when content changes Container Install/update dependencies when repo content changes
postCreateCommand Once — after all of the above Container Install dependencies, seed DB, generate files — the main setup hook
postStartCommand Every container start Container Start background services, print a welcome message
postAttachCommand Every VS Code attach Container Show status, run quick checks that help the developer
json — combining multiple lifecycle hooks
{ // Runs on host — validate environment before container starts "initializeCommand": { "check-docker": "docker info > /dev/null 2>&1 || (echo 'Docker not running' && exit 1)" }, // Runs ONCE in container — install project deps on first create "postCreateCommand": "bash .devcontainer/scripts/postCreate.sh", // Runs on EVERY container start — fast, idempotent operations only "postStartCommand": { "git-status": "git status --short", "greet": "echo '\\n🚀 Dev container ready. Happy coding!\\n'" }, // Runs every time VS Code attaches — useful for quick health checks "postAttachCommand": "python --version && echo 'Python OK'" }
postStartCommand runs every time the container starts — including every time you open VS Code on an already-built container. Never put slow operations (package installs, database migrations) here. Put those in postCreateCommand which only runs once. postStartCommand should be near-instant.
15

Environment Variables & Secrets

// KEEPING SECRETS OUT OF devcontainer.json

Never hardcode secrets in devcontainer.json — it's committed to source control. The correct pattern is to store secrets in a .env file (git-ignored), pass them from the host environment, or use a secrets manager.

Pattern 1 — .env File (Simple, Recommended)

json — devcontainer.json — reference .env file
{ // Reference a local .env file — values go into the container "runArgs": ["--env-file", "${localWorkspaceFolder}/.devcontainer/.env"], // OR use remoteEnv for non-secret config that's OK in source control "remoteEnv": { "DATABASE_URL": "postgresql://devuser:devpass@db:5432/devdb", "ENVIRONMENT": "development", "LOG_LEVEL": "DEBUG", "PYTHONPATH": "${containerWorkspaceFolder}/src" } }
bash — .devcontainer/.env (git-ignored, per-developer)
# .devcontainer/.env — add this path to .gitignore # Copy from .devcontainer/.env.example and fill in your values OPENAI_API_KEY=sk-...your-key-here... STRIPE_SECRET_KEY=sk_test_... AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=... GITHUB_TOKEN=ghp_...
bash — .devcontainer/.env.example (committed to source control)
# Copy to .env and fill in your values # Never commit .env — it is in .gitignore OPENAI_API_KEY= STRIPE_SECRET_KEY= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= GITHUB_TOKEN=

Pattern 2 — Forward Host Environment Variables

json — devcontainer.json — pass host env vars into container
{ "remoteEnv": { // ${localEnv:VAR_NAME} reads from your host shell environment "OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}", "STRIPE_SECRET_KEY": "${localEnv:STRIPE_SECRET_KEY}", "AWS_PROFILE": "${localEnv:AWS_PROFILE}", // containerEnv uses variables already in the container "PYTHONPATH": "${containerWorkspaceFolder}/src:${containerEnv:PYTHONPATH}" } }
Variable interpolation reference: ${localEnv:X} reads from your host shell. ${containerEnv:X} reads from the container environment. ${localWorkspaceFolder} is the project path on the host. ${containerWorkspaceFolder} is the project path inside the container (usually /workspaces/repo-name). ${localWorkspaceFolderBasename} is just the folder name.
16

Mounts & Volumes

// SSH KEYS · NODE_MODULES · .VENV · GIT CONFIG

The workspace bind mount is automatic. Everything else you need from your host — SSH keys, Git config, GPG keys, AWS credentials — must be explicitly mounted. Named volumes are used for package caches (like node_modules or .venv) so they survive container rebuilds.

json — common mounts in devcontainer.json
{ "mounts": [ // ── SSH Keys — so you can push to GitHub from inside the container ─ "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,readonly", // ── Git config — so commits use your name and email ──────────────── "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly", // ── AWS credentials ──────────────────────────────────────────────── "source=${localEnv:HOME}/.aws,target=/home/vscode/.aws,type=bind,readonly", // ── Named volume for Python .venv — fast reinstall if image rebuilds "source=my-project-venv,target=/workspaces/my-project/.venv,type=volume", // ── Named volume for node_modules ────────────────────────────────── "source=my-project-node-modules,target=/workspaces/my-project/node_modules,type=volume", // ── Bind a specific host directory (e.g. shared data files) ──────── "source=${localEnv:HOME}/datasets,target=/home/vscode/datasets,type=bind" ] }

SSH Key Forwarding (Alternative: SSH Agent)

json — devcontainer.json — SSH agent forwarding
{ // Instead of copying SSH keys into the container, // forward your host SSH agent — more secure, key never leaves host "remoteEnv": { "SSH_AUTH_SOCK": "/run/host-services/ssh-auth.sock" }, "mounts": [ "source=/run/host-services/ssh-auth.sock,target=/run/host-services/ssh-auth.sock,type=bind" ] }
Named volumes and macOS performance: Named volumes (type=volume) live inside the Docker VM on macOS and have no I/O overhead. Use them for node_modules, .venv, and any directory with many small files. Bind mounts (type=bind) for your source code are slower on macOS but Docker Desktop's VirtioFS minimises this significantly.
17

Non-Root Users

// SECURITY BEST PRACTICE FOR DEV CONTAINERS

Microsoft's official devcontainer images include a pre-built non-root user: vscode (for most images) or node (for Node images). Running as a non-root user is a security best practice — it limits the blast radius if a dependency is compromised, and it's required for some corporate security policies.

dockerfile — adding a non-root user to a custom image
FROM ubuntu:24.04 ARG USERNAME=devuser ARG USER_UID=1000 ARG USER_GID=$USER_UID # Install basic tools RUN apt-get update && apt-get install -y \ sudo curl git bash-completion \ && rm -rf /var/lib/apt/lists/* # Create the non-root user RUN groupadd --gid $USER_GID $USERNAME \ && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ # Allow the user to use sudo without a password (development only!) && echo "$USERNAME ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \ && chmod 0440 /etc/sudoers.d/$USERNAME # Install Python as root, then switch to the user RUN apt-get update && apt-get install -y python3.12 python3-pip \ && rm -rf /var/lib/apt/lists/* USER $USERNAME WORKDIR /home/$USERNAME
json — devcontainer.json with non-root user
{ "name": "Secure Python", "build": { "dockerfile": "Dockerfile", "args": { "USERNAME": "devuser", "USER_UID": "1000", "USER_GID": "1000" } }, // Tell VS Code to run as this user "remoteUser": "devuser", // The container process itself can still run as root if needed (for Docker commands) // but VS Code connects as remoteUser "containerUser": "root" }
remoteUser vs containerUser: remoteUser is the user VS Code connects as — your terminal, extensions, and git operations run as this user. containerUser is the user the Docker container process starts as. For most projects, set both to the same non-root user. Only set containerUser to root if you need to run Docker-in-Docker.
18

GitHub Codespaces Compatibility

// SAME CONFIG, CLOUD-HOSTED ENVIRONMENT

GitHub Codespaces reads the same .devcontainer/devcontainer.json your local Dev Containers extension uses. If your dev container works locally, it works in Codespaces — with no additional configuration. This makes it trivially easy to give contributors (or your own team) a browser-based or SSH-based cloud development environment.

What Works Identically
  • All devcontainer.json fields
  • Features, lifecycle scripts, extensions
  • Port forwarding (via HTTPS tunnel)
  • Environment variables from repo secrets
  • Docker Compose multi-service setups
Codespaces-Specific Considerations
  • Prebuilds: build the image once, reuse on new Codespace create
  • Machine type: choose 2-core / 4-core / 8-core / etc.
  • Secrets: set via GitHub repo settings → Codespaces secrets
  • Dotfiles: Codespaces applies your personal dotfiles repo
  • Cost: billed per compute-hour; stop when not in use

Codespaces-Specific devcontainer.json Fields

json — devcontainer.json with Codespaces config
{ "name": "My Project", "image": "mcr.microsoft.com/devcontainers/python:3.12", // Codespaces-specific: configure the default machine type "hostRequirements": { "cpus": 4, "memory": "8gb", "storage": "32gb" }, // Codespaces-specific: which ports to show in VS Code / browser "forwardPorts": [8000], "portsAttributes": { "8000": { "label": "API", "onAutoForward": "openPreview", // In Codespaces, make ports public for sharing with reviewers "visibility": "public" } }, // Codespaces-specific: environment variables (non-secret) "remoteEnv": { "ENVIRONMENT": "codespaces" }, // Secret env vars → GitHub → Settings → Secrets → Codespaces secrets // (they appear as env vars automatically — no devcontainer.json needed) "postCreateCommand": "pip install -r requirements.txt", "customizations": { "vscode": { "extensions": ["ms-python.python", "charliermarsh.ruff"] }, // Codespaces web editor (github.dev) extensions "codespaces": { "openFiles": ["README.md"] // open this file when the Codespace starts } } }
Prebuilds dramatically speed up Codespace creation. Without prebuilds, every new Codespace builds the image from scratch. With prebuilds (configured in GitHub repo settings), GitHub runs the container build periodically, and new Codespaces start from the cached image in seconds. Essential for repos with large postCreateCommand steps.
19

Troubleshooting

// COMMON ISSUES AND HOW TO FIX THEM
ProblemLikely CauseFix
Container fails to build Dockerfile error or network issue pulling base image Check output in VS Code terminal. Run docker build .devcontainer manually to see full error. Check Docker Hub / GHCR connectivity.
Hot reload not working (React/Vite) Dev server bound to localhost instead of 0.0.0.0 Add server.host: '0.0.0.0' to vite.config.ts. Add --hostname 0.0.0.0 for Next.js. Enable usePolling: true in Vite if still broken on macOS.
Port forwarding not working App not bound to all interfaces or port not in forwardPorts Ensure the app binds to 0.0.0.0, not 127.0.0.1. Add the port to forwardPorts in devcontainer.json.
Slow file I/O on macOS Docker Desktop using old file sharing implementation Enable VirtioFS in Docker Desktop → Settings → General. Also add :cached to bind mounts in docker-compose.yml.
Python extensions not finding interpreter Interpreter path in settings doesn't match where venv was created Set python.defaultInterpreterPath to the exact path: /workspaces/repo/.venv/bin/python. Run which python in the container terminal to verify.
node_modules empty after rebuild Named volume wasn't used — modules lived in container layer Add a named volume mount targeting node_modules. On first open after adding it, run npm ci manually.
Can't push to GitHub (auth error) SSH keys not mounted or SSH agent not forwarded Mount ~/.ssh read-only, or set up SSH agent forwarding. Check with ssh -T git@github.com inside the container.
postCreateCommand failing silently Script exits with error but devcontainer doesn't stop Add set -e to the top of your shell script so any error aborts the script. Check "Dev Containers: Show Container Log" in VS Code command palette.
Extension not installing in container Extension ID typo or extension not compatible with remote Verify the ID from the VS Code marketplace URL. Some extensions must be installed locally (UI extensions) rather than remotely.
"Docker daemon not running" Docker Desktop not started Start Docker Desktop. On Linux, run sudo systemctl start docker.

Useful Debug Commands

bash — debugging inside the container
# Check which user you're running as whoami # Check Python path and version which python && python --version # Check Node version node --version && npm --version # See all environment variables env | sort # Test database connectivity (for Compose setups) pg_isready -h db -U devuser -d devdb # Check what's listening on a port ss -tlnp | grep 8000 # View the devcontainer configuration VS Code is using # → VS Code command palette: "Dev Containers: Show Container Log" # Rebuild the container without cache # → VS Code command palette: "Dev Containers: Rebuild Container Without Cache" # CLI: test build locally devcontainer build --workspace-folder . --no-cache
20

Cheat Sheet

// QUICK REFERENCE FOR DAILY USE

VS Code Command Palette Actions

CommandWhat it does
Dev Containers: Reopen in ContainerStart the dev container (or switch to it)
Dev Containers: Rebuild ContainerRebuild the image and restart (uses cache)
Dev Containers: Rebuild Container Without CacheFull clean rebuild — use after changing Dockerfile
Dev Containers: Reopen Folder LocallyExit the container, return to local workspace
Dev Containers: Open Container Configuration FileJump to devcontainer.json
Dev Containers: Show Container LogSee build output, lifecycle script output
Dev Containers: Clone Repository in Container VolumeClone a repo into a Docker volume (maximum performance)
Dev Containers: Add Dev Container Configuration FilesScaffold a devcontainer.json from a template

devcontainer.json Quick-Start Templates

Minimal — Image Only
{ "name": "My Project", "image": "mcr.microsoft.com/devcontainers/python:3.12", "postCreateCommand": "pip install -r requirements.txt", "remoteUser": "vscode" }
Standard — Dockerfile + Extensions
{ "name": "My Project", "build": { "dockerfile": "Dockerfile" }, "forwardPorts": [8000], "customizations": { "vscode": { "extensions": ["ms-python.python"] } }, "postCreateCommand": "bash .devcontainer/scripts/postCreate.sh", "remoteUser": "vscode" }

Microsoft Official Base Images

StackImage
Python 3.12mcr.microsoft.com/devcontainers/python:3.12
Python 3.11mcr.microsoft.com/devcontainers/python:3.11
Node 22 + TypeScriptmcr.microsoft.com/devcontainers/typescript-node:22
Node 20mcr.microsoft.com/devcontainers/javascript-node:20
Go 1.22mcr.microsoft.com/devcontainers/go:1.22
Rustmcr.microsoft.com/devcontainers/rust:latest
Java 21mcr.microsoft.com/devcontainers/java:21
Ubuntu 24.04mcr.microsoft.com/devcontainers/base:ubuntu-24.04
Debian Bookwormmcr.microsoft.com/devcontainers/base:bookworm
Universal (all languages)mcr.microsoft.com/devcontainers/universal:2

Reference Links

Official Spec

containers.dev — the open specification for Dev Containers, maintained by Microsoft, with schema reference and feature registry.

VS Code Docs

code.visualstudio.com/docs/devcontainers — full documentation on Dev Containers, advanced configuration, and troubleshooting guides.

Template Gallery

github.com/devcontainers/templates — official templates for Python, Node, Go, Rust, Java, and more. A great starting point for any project.

Commit your devcontainer config on day one. The best time to add a .devcontainer folder to a project is when you start it. The second best time is today. Every day without one means one more engineer with a subtly different local environment, one more "it works locally" bug, and one more onboarding doc that's already out of date.
Back to Handbooks