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. In 2026, the same primitive has become the default sandbox boundary for autonomous coding agents like Claude Code and Codex.
--dangerously-skip-permissions on the host for most teams this year. Documented the spec's new secrets property for declarative secret requirements, digest-pinning as the 2026 baseline over version tags, the devcontainer CLI's current-directory default workspace behavior and git-worktree mount support, and refreshed Feature/base-image references throughout.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.
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.
A new engineer clones the repo, opens VS Code, clicks "Reopen in Container" and has a fully functional development environment in minutes — not days.
Project tools, runtimes, and dependencies live inside the container. Your host machine stays clean. Switching between projects with conflicting requirements is trivial.
The dev environment is defined in .devcontainer/devcontainer.json — committed to version control, code-reviewed, and updated just like application code.
The same devcontainer.json powers GitHub Codespaces, VS Code Remote SSH, and CI pipelines — one definition, used everywhere.
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
| Dimension | Traditional Setup | Dev Container |
|---|---|---|
| Onboarding time | Hours to days of manual steps | Minutes — clone and reopen |
| Environment parity | "Works on my machine" syndrome | Guaranteed identical for all developers |
| Dependency conflicts | Global tool versions clash between projects | Isolated per project inside container |
| Documentation | README setup guides that go stale | devcontainer.json — always current, executable |
| OS compatibility | Different scripts for Mac, Linux, Windows | Single definition works everywhere |
| CI parity | CI environment differs from local | Same container used locally and in CI |
| Extension consistency | Each dev has different VS Code extensions | Extensions defined and auto-installed |
| Host machine health | Accumulates global tools, versions, clutter | Host stays clean — tools in container |
When To Use Dev Containers
- 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
- Solo developer, single OS, simple project with no conflicts
- Team uses powerful native builds where Docker overhead matters
- Project requires host GPU access (gaming, native ML — check GPU passthrough)
- Offline development where pulling images is not possible
- Native iOS/macOS development requiring Xcode (macOS-specific)
- Team lacks Docker fundamentals and learning curve is too high
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.
┌─ 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 │ └─────────────────────────────────────────────────────────────────┘
File Structure
The Dev Container specification looks for configuration in these locations, in order of precedence. Most projects use .devcontainer/devcontainer.json.
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
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.
{
"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
},
// ── DECLARATIVE SECRETS (spec addition) ──────────────
// Declares required secrets WITHOUT storing values in the file.
// Supporting tools (Codespaces, CLI) prompt for or inject these.
"secrets": {
"OPENAI_API_KEY": { "description": "Key for local LLM calls in tests" },
"DATABASE_PASSWORD": { "description": "Seed DB password, not the prod one" }
},
// ── 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
| Property | Type | Purpose |
|---|---|---|
| name | string | Display name shown in VS Code and Codespaces UI |
| image | string | Docker image reference — DockerHub, GHCR, MCR, ACR. Pin to a digest (@sha256:...) for fully reproducible builds, not just a version tag. |
| secrets | object | Declares required secrets by name and description without storing values — supporting tools prompt for or securely inject them at container creation, no rebuild needed to rotate |
| build.dockerfile | string | Path to Dockerfile relative to devcontainer.json |
| dockerComposeFile | string | array | Docker Compose file(s) for multi-service setups |
| service | string | Which Compose service VS Code connects to |
| features | object | Add tooling Features from the OCI registry |
| forwardPorts | number[] | Ports auto-forwarded to localhost on open |
| portsAttributes | object | Labels and auto-forward behavior per port |
| containerEnv | object | Static env vars baked into the container at creation |
| remoteEnv | object | Dynamic env vars for terminals and VS Code processes only |
| remoteUser | string | User VS Code Server and terminals run as inside container |
| mounts | string[] | Additional bind mounts or named volumes |
| workspaceFolder | string | Path inside container where VS Code opens (default: /workspaces/name) |
| customizations.vscode.extensions | string[] | Extensions auto-installed in the container |
| customizations.vscode.settings | object | VS Code settings scoped to this container |
| runArgs | string[] | Extra Docker run arguments (capabilities, security opts) |
| hostRequirements | object | Minimum host memory/CPU — used by Codespaces for sizing |
| updateRemoteUserUID | boolean | Sync container user UID/GID with host user (Linux only) |
Variable Substitution
| Variable | Resolves 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) |
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.
Popular Official Features
| Feature | OCI identifier | What it installs |
|---|---|---|
| Node.js | ghcr.io/devcontainers/features/node:1 | Node.js, npm, nvm. Accepts version, nodeGypDependencies |
| Python | ghcr.io/devcontainers/features/python:1 | Python, pip, pyenv. Accepts version |
| Go | ghcr.io/devcontainers/features/go:1 | Go toolchain. Accepts version |
| Rust | ghcr.io/devcontainers/features/rust:1 | Rust, cargo, rustup |
| Java | ghcr.io/devcontainers/features/java:1 | JDK via SDKMAN. Accepts version, jdkDistro |
| .NET | ghcr.io/devcontainers/features/dotnet:2 | .NET SDK. Accepts version |
| Git | ghcr.io/devcontainers/features/git:1 | Latest git, git-lfs, config helpers |
| GitHub CLI | ghcr.io/devcontainers/features/github-cli:1 | gh CLI tool |
| Docker-in-Docker | ghcr.io/devcontainers/features/docker-in-docker:2 | Full Docker daemon inside the container |
| Docker-outside-Docker | ghcr.io/devcontainers/features/docker-outside-docker:1 | Docker socket bind mount (simpler) |
| kubectl | ghcr.io/devcontainers/features/kubectl-helm-minikube:1 | kubectl, helm, optional minikube |
| Terraform | ghcr.io/devcontainers/features/terraform:1 | Terraform, tflint, terragrunt |
| Azure CLI | ghcr.io/devcontainers/features/azure-cli:1 | az CLI |
| AWS CLI | ghcr.io/devcontainers/features/aws-cli:1 | AWS CLI v2 |
"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": {} }
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.
| Hook | Runs when | Frequency | Use for |
|---|---|---|---|
| initializeCommand | On host, before container starts | Every start | Host-side validation, creating local dirs |
| onCreateCommand | Inside container, after creation | Once at creation | Tool configuration, one-time installs |
| updateContentCommand | After content update (prebuilds) | When content changes | Dependency install tied to project files |
| postCreateCommand | After all create commands finish | Once at creation | npm/pip install, DB migrations, .env setup |
| postStartCommand | Every container start | Every start | Start a dev server, apply git config |
| postAttachCommand | When a tool attaches to the container | Each attach | Show welcome messages, open browser |
// 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"
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.
"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.
"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" ]
Image vs Dockerfile vs Compose
| Approach | Use when | Pros | Cons |
|---|---|---|---|
| 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)
| Image | Tag pattern | Includes |
|---|---|---|
mcr.microsoft.com/devcontainers/base | :ubuntu :debian :alpine | Base OS + common tools (git, curl, zsh, etc.) |
mcr.microsoft.com/devcontainers/javascript-node | :22 :20 | Node.js LTS, npm, nvm, yarn |
mcr.microsoft.com/devcontainers/python | :3.12 :3.11 | Python, pip, venv, pylint, black |
mcr.microsoft.com/devcontainers/dotnet | :8.0 :9.0 | .NET SDK, C# tools |
mcr.microsoft.com/devcontainers/go | :1.23 | Go, gopls, delve debugger |
mcr.microsoft.com/devcontainers/rust | :latest | Rust toolchain, cargo, rust-analyzer |
mcr.microsoft.com/devcontainers/java | :21 | Java JDK, Maven, Gradle |
mcr.microsoft.com/devcontainers/universal | :linux | Node, Python, Go, Java, .NET, Ruby, PHP — used by Codespaces default |
Sample: Node.js / TypeScript
node_modules volume for performance.{
"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: Python / FastAPI
{
"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" }
}
}
}
}
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: .NET 8 / C#
{
"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"]
}
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: Multi-Service Stack
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:
{
"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: GitHub Codespaces
hostRequirements, Codespaces-specific settings, and prebuild strategy are used for cloud optimization.{
"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"]
}
}
}
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.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.
# 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 # 2026: commands default to the current directory when # --workspace-folder is omitted — handy for scripting and agent tooling cd ./my-project && devcontainer up # Mount a git worktree's common .git dir — lets a worktree checkout # run inside its own dev container while sharing history with the main clone devcontainer up --workspace-folder ../my-project-worktree --mount-workspace-git-root
devcontainer CLI releases use the current working directory as the default workspace folder when --workspace-folder is not specified, and added an option to mount a git worktree's common folder — both changes aimed squarely at scripted and agentic usage, where a loop or subagent cds into a worktree and just runs devcontainer up with no extra flags.Using Dev Containers in CI (GitHub Actions)
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
AI Agent Sandboxing
By mid-2026, dev containers became the default sandbox boundary for autonomous coding agents. Claude Code, Codex, and similar tools work fastest when they can act without asking permission on every file write or shell command — but granting that on your host machine means the agent has full read/write over your ~/.ssh, cloud credentials, and every other project on disk. A dev container gives it a disposable, isolated Linux environment instead: full autonomy inside, no reach outside.
--dangerously-skip-permissions (Claude Code) and --sandbox danger-full-access (Codex) remove per-action approval so an agent can run for hours unattended. That's the point of loop-driven agentic workflows — but on a bare host it also means one bad rm -rf or a prompt-injected instruction from untrusted file content executes with your full user permissions. Containing the blast radius to a devcontainer is the standard mitigation teams converged on this year.vscode user out of the box; use it.git worktree isolation so parallel agent sessions each get their own working copy. No merge conflicts between agents; review and merge each branch when it finishes.remoteEnv), never the full ~/.ssh or cloud credential directory. Use the spec's secrets property to declare what's required.Reference Dockerfile Pattern
FROM mcr.microsoft.com/devcontainers/python:1-3.12-bookworm ARG USERNAME=vscode # persisted shell history across rebuilds — helps agent + human context RUN mkdir /commandhistory \ && touch /commandhistory/.bash_history \ && chown -R $USERNAME /commandhistory \ && echo 'export HISTFILE=/commandhistory/.bash_history' \ >> /home/$USERNAME/.bashrc # dirs the agent writes into (skills, memory, session state) — non-root owned RUN mkdir -p /home/$USERNAME/.claude /home/$USERNAME/.codex \ && chown -R $USERNAME /home/$USERNAME/.claude /home/$USERNAME/.codex # install the agent CLIs themselves RUN npm install -g @anthropic-ai/claude-code @openai/codex USER $USERNAME
| Practice | Why it matters |
|---|---|
Bind-mount ~/.claude / ~/.codex, not the whole home dir |
Preserves the agent's skills, memory, and session state across rebuilds without exposing unrelated host files. |
| Firewall / egress restriction | Some hardened setups lock devcontainer.json and a firewall script with chattr +i so the agent itself can't disable its own network restrictions mid-session. |
| One-time token setup for headless use | Run claude setup-token on the host once, export the resulting token as a build arg or secret, then rebuild — avoids interactive login inside a container running unattended in CI or a loop. |
| Treat untrusted file content as untrusted input | Issue bodies, PR descriptions, and third-party file contents read into agent context can carry prompt injection. Sandbox isolation is a backstop, not a substitute for validating tool-call arguments. |
| Editor on host, agent in container | A common split: edit files with VS Code / your IDE running on the host as normal, while the agent process itself runs isolated inside the container with no editor UI processes of its own. |
postCreateCommand, mounts, remoteUser, and Features work exactly as documented elsewhere in this handbook. What changed is who's inside the container: a human developer, an autonomous loop, or both taking turns. Design the container the same way regardless — least privilege, non-root, pinned images, committed config.Tips & Tricks
| Tip | Detail |
|---|---|
| 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. |
| Digest-lock for full reproducibility | The 2026 baseline for teams that need byte-for-byte reproducible environments (including AI-agent sandboxes) goes one step further: pin the image by digest, e.g. image@sha256:1a2b3c.... A version tag can still be re-pushed upstream; a digest cannot change under you. |
| 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. |
Anti-Patterns & Pitfalls
| Anti-pattern | Problem | Fix |
|---|---|---|
| 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. |
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:22or:3.12-bookworm. - Dependency install in lifecycle hook —
npm install/pip installinpostCreateCommand, not in the Dockerfile. - No secrets in containerEnv — using
remoteEnvwith${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: trueset 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_modulesfor 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.
- Safe for autonomous agent use (if applicable) — non-root remoteUser, only the credentials the agent needs are mounted, and no direct path to host
~/.sshor cloud credentials.