
Devcontainers
- 18 installs
- 22 repo stars
- Updated February 19, 2026
- markpitt/claude-skills
Helps with ai & agent building tasks.
About
devcontainers is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- devcontainers
- AI & Agent Building
- AI-coding skill
Devcontainers by the numbers
- 18 all-time installs (skills.sh)
- Ranked #10,710 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/markpitt/claude-skills --skill devcontainersAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 22 |
| Last updated | February 19, 2026 |
| Repository | markpitt/claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Dev Containers Skill
Expert guidance on creating, configuring, and optimising containerised development environments using the Dev Container Specification. Covers devcontainer.json authoring, Features, Templates, performance, security, multi-container setups, and cloud environments.
Quick Reference Table
| Task | Load Resource | Key Concepts |
|---|---|---|
Create or configure a devcontainer.json | references/core-concepts.md | image, build, features, lifecycle hooks, customizations |
| Add tools/languages to a container | references/features-templates.md | Features, version pinning, installsAfter, Templates |
| Set up multi-container or Docker-in-Docker | references/advanced-config.md | dockerComposeFile, DinD, DooD, Kubernetes, service |
| Improve build/startup speed | references/performance-security.md | layer caching, named volumes, pre-built images, Virtio-fs |
| Harden container security or manage secrets | references/performance-security.md | remoteUser, UID mapping, SSH forwarding, secrets |
| Debug slow mounts, permission errors, credential issues | references/troubleshooting.md | UID/GID, bind mounts, SSH agent, postCreateCommand |
| Integrate with Codespaces or DevPod | references/advanced-config.md | prebuilds, providers, SSH, cloud residency |
Orchestration Protocol
Phase 1 — Classify the Task
Identify which category the user's request falls into:
- Configuration — writing or editing
devcontainer.json, Dockerfiles, Compose files - Tooling — adding Features, authoring custom Features, or using Templates
- Advanced — multi-container, DinD/DooD, Kubernetes, cloud environments
- Optimisation — caching, pre-built images, named volumes, disk I/O
- Security — non-root users, UID mapping, secrets, hardened images
- Troubleshooting — permission errors, slow builds, SSH/GPG credential issues
Phase 2 — Load the Right Resource
Load the resource indicated in the Quick Reference Table. For complex tasks spanning multiple areas (e.g. "set up a secure multi-container environment with fast builds"), load both relevant files.
Phase 3 — Execute
Apply the guidance from the loaded resource. Use concrete examples from the resource files. For configuration tasks, produce a complete, commented devcontainer.json snippet.
Common Task Workflows
Workflow 1: Create a New Dev Container from Scratch
1. Load references/core-concepts.md for the full property reference 2. Choose orchestration method: image (simplest), build.dockerfile, or dockerComposeFile 3. Add features for tools (Node.js, Python, Git, Docker CLI, etc.) 4. Set remoteUser to a non-root user for security 5. Add customizations.vscode.extensions and settings for IDE consistency 6. Add postCreateCommand to install dependencies automatically 7. Add forwardPorts for any app ports
{
"name": "My Project",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "20" },
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": {
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"],
"settings": { "editor.formatOnSave": true }
}
},
"postCreateCommand": "npm install",
"forwardPorts": [3000],
"remoteUser": "node"
}Workflow 2: Add a Tool via Features
1. Load references/features-templates.md 2. Browse the official registry or GitHub Container Registry 3. Add the feature with a pinned version to devcontainer.json 4. Use overrideFeatureInstallOrder if ordering matters
Workflow 3: Multi-Container Setup with Docker Compose
1. Load references/advanced-config.md 2. Create docker-compose.yml for services (app, database, cache) 3. Create docker-compose.dev.yml for development overrides (source mounts, debug ports) 4. Set dockerComposeFile to both files in devcontainer.json 5. Set service to the container the IDE should attach to 6. Set shutdownAction: "none" to keep services running when IDE closes
Workflow 4: Speed Up Dev Container Builds
1. Load references/performance-security.md 2. Order Dockerfile instructions from least-changed to most-changed 3. Use RUN --mount=type=cache for package managers 4. Use named volumes for node_modules / heavy dependency directories 5. Pre-build and push image in CI; reference the pre-built image in devcontainer.json
Workflow 5: Troubleshoot a Permission or Credential Issue
1. Load references/troubleshooting.md 2. For UID mismatch: ensure updateRemoteUserUID: true (default on Linux) 3. For SSH: ensure local SSH agent is running; the extension auto-forwards it 4. For Git inside container: use SSH agent forwarding; do not copy private keys
Resource Summaries
| File | Contents | Lines |
|---|---|---|
references/core-concepts.md | Full devcontainer.json property reference, lifecycle hooks, location precedence | ~280 |
references/features-templates.md | Consuming and authoring Features, Templates distribution, version pinning | ~260 |
references/advanced-config.md | Multi-container, Docker-in-Docker/from-Docker, Kubernetes, Codespaces, DevPod | ~280 |
references/performance-security.md | Layer caching, named volumes, pre-built images, non-root users, secrets | ~270 |
references/troubleshooting.md | Permission errors, slow I/O, SSH/GPG credentials, lifecycle script issues | ~200 |
Best Practices
- Version-pin everything — pin Features (
feature:1) and base images (python:3.12) for reproducibility - Non-root by default — always set
remoteUserto a non-root user; useupdateRemoteUserUID: trueon Linux - Automate setup — use
postCreateCommandto install dependencies so the environment is immediately usable - Don't modify production Compose — use a
docker-compose.dev.ymloverride for dev-specific additions - Pre-build images in CI — reduces startup from minutes to seconds; embed metadata in image labels
- Never bake secrets into images — use SSH agent forwarding, BuildKit secret mounts, or
.envfiles (git-ignored) - Named volumes for heavy directories — on macOS/Windows, mount
node_modulesetc. into named volumes for native I/O speed
External References
- Dev Container Specification — official specification and schema
- devcontainer.json reference — full property reference
- Official Features registry — browse available Features
- Official Templates registry — browse available Templates
- VS Code Dev Containers docs — IDE integration guide
- GitHub Codespaces docs — cloud-hosted containers
- DevPod docs — open-source provider-agnostic cloud dev environments
- devcontainers/cli — reference CLI implementation
Advanced Dev Container Configuration
Multi-container setups, Docker-in-Docker, Kubernetes integration, and cloud environments.
---
Multi-Container with Docker Compose
Basic Setup
// .devcontainer/devcontainer.json
{
"name": "My App",
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.dev.yml"],
"service": "app",
"workspaceFolder": "/workspace",
"shutdownAction": "none",
"forwardPorts": [3000, 5432, 6379]
}# docker-compose.yml (production — do not modify for dev)
services:
app:
build: .
ports:
- "3000:3000"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
cache:
image: redis:7-alpine# .devcontainer/docker-compose.dev.yml (dev overrides)
services:
app:
volumes:
# Mount source code instead of copying it
- ..:/workspace:cached
command: sleep infinity # Keep container alive; start app manually
environment:
DEBUG: "true"
ports:
- "9229:9229" # Node.js debugger port
db:
volumes:
- db-data:/var/lib/postgresql/data # Persist DB data
volumes:
db-data:Key Properties for Compose Setups
| Property | Description |
|---|---|
service | Which Compose service the IDE should attach to (required) |
workspaceFolder | Working directory inside the container |
shutdownAction | "none" keeps all services running when IDE closes; "stopCompose" stops all |
runServices | Array of service names to start; omit to start all |
{
"runServices": ["app", "db"], // Don't start cache unless needed
"shutdownAction": "none"
}Network Isolation
services:
app:
networks:
- frontend
- backend
db:
networks:
- backend # DB not reachable from frontend network
nginx:
networks:
- frontend
networks:
frontend:
backend:
internal: true # No external internet accessMonorepo with Multiple Dev Containers
repo/
├── .devcontainer/
│ ├── backend/
│ │ └── devcontainer.json # { "service": "backend", "dockerComposeFile": ["../../docker-compose.yml"] }
│ └── frontend/
│ └── devcontainer.json # { "service": "frontend", "dockerComposeFile": ["../../docker-compose.yml"] }---
Docker-in-Docker (DinD) vs. Docker-outside-of-Docker (DooD)
Docker-in-Docker (DinD)
Runs a separate Docker daemon inside the container. Completely isolated from the host.
Use when: CI/CD pipelines, isolated build environments, Kubernetes (where the host socket may not be accessible)
{
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": true,
"dockerDashComposeVersion": "v2"
}
},
"privileged": true // Required for DinD
}Downsides: No shared layer cache with host; images don't persist across container rebuilds by default.
Docker-outside-of-Docker (DooD)
Mounts the host's Docker socket so the container controls the host's Docker daemon.
Use when: Development, when you want to share the host's image cache.
{
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
}
// No "privileged" needed — socket mounting is sufficient
}# If using Compose
services:
app:
volumes:
- /var/run/docker.sock:/var/run/docker.sockDownsides: Containers started inside the dev container are siblings on the host, not children — path mapping can be tricky.
Comparison
| DinD | DooD | |
|---|---|---|
| Isolation | Full | Shares host daemon |
| Privileged required | Yes | No |
| Shares host image cache | No | Yes |
| Build speed | Slower (cold cache) | Faster |
| CI suitability | Excellent | Good |
---
Kubernetes Integration
Install kubectl + Helm
{
"features": {
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
"version": "latest",
"helm": "latest",
"minikube": "none" // "none" to skip Minikube
}
}
}Connect to Existing Cluster
Mount the host's ~/.kube:
{
"mounts": [
"source=${localEnv:HOME}/.kube,target=/home/vscode/.kube,type=bind,consistency=cached"
]
}Or copy kubeconfig in postCreateCommand:
{
"postCreateCommand": "mkdir -p ~/.kube && cp /host-kube/config ~/.kube/config"
}Minikube Inside DinD
{
"features": {
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
"minikube": "latest"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"privileged": true
}Start Minikube with the docker driver inside the container:
minikube start --driver=dockerDevSpace — Deploy to Remote Cluster
DevSpace allows developing directly against a Kubernetes cluster:
# Install DevSpace
curl -L -o devspace \
"https://github.com/loft-sh/devspace/releases/latest/download/devspace-linux-amd64"
# Sync local files to a pod
devspace devUseful for workloads that need cloud resources (GPUs, large datasets) unavailable locally.
---
GitHub Codespaces
Codespaces uses devcontainer.json natively — the same file that works locally works in Codespaces.
Codespaces-Specific Configuration
{
"customizations": {
"codespaces": {
"repositories": {
"myorg/private-repo": {
"permissions": { "contents": "read" }
}
}
}
}
}Secrets
Prompt users to provide secrets at creation time:
{
// In .devcontainer/devcontainer.json
// Users set these in their Codespaces user settings or org settings
"containerEnv": {
"MY_API_KEY": "${localEnv:MY_API_KEY}"
}
}Define recommended secrets in the repository (Settings → Codespaces → Secrets). The secret value is injected but never logged.
Prebuilds
Enable prebuilds to reduce startup from ~2-5 minutes to ~10-20 seconds:
1. Go to Settings → Codespaces → Prebuild configuration 2. Select branch and region 3. Codespaces runs onCreateCommand and updateContentCommand ahead of time
The prebuild caches postCreateCommand output up to but not including postAttachCommand. Write postCreateCommand to be idempotent.
Machine Types
Codespaces machine types (set via hostRequirements):
{
"hostRequirements": {
"cpus": 4,
"memory": "8gb",
"storage": "32gb"
}
}Data Residency (Enterprise)
Enterprise organisations can restrict Codespaces to specific geographic regions for compliance. Configure in the GitHub Enterprise admin console.
---
DevPod — Provider-Agnostic Dev Environments
DevPod is an open-source, client-only tool that runs dev containers on any backend (local Docker, AWS, Azure, GCP, Kubernetes, SSH).
Install
# macOS
brew install loft-sh/tap/devpod
# Linux
curl -L -o devpod \
"https://github.com/loft-sh/devpod/releases/latest/download/devpod-linux-amd64"
chmod +x devpod && sudo mv devpod /usr/local/bin/Providers
# List providers
devpod provider list
# Add AWS provider
devpod provider add aws
# Add Kubernetes provider
devpod provider add kubernetesCreate a Workspace
# From a GitHub repo
devpod up github.com/myorg/my-project --provider aws
# From local directory
devpod up . --provider docker
# Switch provider without changing devcontainer.json
devpod up . --provider kubernetesIDE Integration
DevPod manages SSH automatically — connect any IDE:
# Open in VS Code
devpod up . --ide vscode
# Open in JetBrains (requires Gateway)
devpod up . --ide intellij
# Open in Zed
devpod up . --ide zed
# SSH terminal access
devpod ssh my-workspaceKey Advantages over Codespaces
| Feature | Codespaces | DevPod |
|---|---|---|
| Provider-agnostic | No (GitHub only) | Yes (any cloud/local) |
| IDE flexibility | VS Code / JetBrains | Any SSH-compatible IDE |
| Open source | No | Yes |
| Cost | GitHub pricing | Pay your own cloud |
| No vendor lock-in | No | Yes |
---
JetBrains IDE Support
JetBrains Gateway connects to dev containers via SSH. The container needs an SSH server, which DevPod handles automatically.
For JetBrains CodeCanvas (cloud IDE), see: https://www.jetbrains.com/code-canvas/
For IntelliJ IDEA with remote containers:
# Start via DevPod with JetBrains IDE
devpod up . --ide intellij---
WSL2 (Windows Dev Setup)
When using Docker Desktop on Windows:
{
// Ensure the project lives inside the WSL2 filesystem for acceptable I/O
// Path: \\wsl$\Ubuntu\home\user\myproject
"workspaceFolder": "/home/vscode/workspace",
"mounts": [
// Avoid mounting Windows-side (C:\) paths directly — slow bind mounts
"source=project-node-modules,target=/home/vscode/workspace/node_modules,type=volume"
]
}Store project files inside WSL2 (~/ inside the Linux distro), not on the Windows filesystem, to avoid bind mount performance penalties.
Dev Container Core Concepts
Reference for devcontainer.json structure, properties, and lifecycle management.
---
What is a Dev Container?
A development container is a Docker-based environment defined as code. The devcontainer.json file is a metadata manifest telling supporting tools (VS Code, GitHub Codespaces, JetBrains, the Dev Container CLI) how to build, start, and configure a containerised development environment.
Key distinction: devcontainer.json enriches a container for development — adding IDE settings, extensions, user permissions, and lifecycle automation that don't belong in production images.
---
File Location Precedence
Tools search for configuration in this order:
| Priority | Location |
|---|---|
| 1 (highest) | .devcontainer/<subfolder>/devcontainer.json |
| 2 | .devcontainer/devcontainer.json |
| 3 (lowest) | .devcontainer.json (root) |
Use subfolders for monorepos with multiple environments (e.g. .devcontainer/backend/, .devcontainer/frontend/).
The file uses JSONC (JSON with Comments) format — // and /* */ comments are allowed.
---
Orchestration Methods (Choose One)
Every devcontainer.json must declare exactly one environment source:
image — Simplest
{
"image": "mcr.microsoft.com/devcontainers/python:3.12"
}References a pre-built image from a registry. Best for quick starts and teams using pre-built base images.
build — Custom Dockerfile
{
"build": {
"dockerfile": "Dockerfile",
"context": "..",
"args": { "VARIANT": "3.12" }
}
}dockerfile: Path relative to.devcontainer/context: Build context (defaults to.devcontainer/)args: Build arguments passed toARGinstructions
dockerComposeFile — Multi-Container
{
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.dev.yml"],
"service": "app",
"workspaceFolder": "/workspace"
}See references/advanced-config.md for full multi-container guidance.
---
Features
Add modular tools without writing Dockerfile instructions:
{
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "20" },
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
}
}- Always pin a major version (
:1,:2) — never use:latest - Control install order with
overrideFeatureInstallOrderif one Feature depends on another
See references/features-templates.md for authoring and advanced usage.
---
Lifecycle Hooks
Scripts that run at defined points in the container lifecycle:
| Hook | When It Runs | Where | Typical Use |
|---|---|---|---|
initializeCommand | Before container is created | Host | Create host directories, set permissions |
onCreateCommand | Once, after container is first created | Container | Clone repos, bootstrap databases |
updateContentCommand | After create; when source changes (Codespaces prebuilds) | Container | Regenerate derived assets |
postCreateCommand | Once, after updateContentCommand | Container | Install dependencies (npm install, pip install) |
postStartCommand | Every container start | Container | Start background services, daemons |
postAttachCommand | Every time an IDE attaches | Container | Show welcome message, open files |
Important: initializeCommand runs on the host; all others run inside the container.
String vs. Array vs. Object Forms
All hooks accept three forms:
// String (runs via shell)
"postCreateCommand": "npm install && npm run build"
// Array (no shell expansion; preferred for safety)
"postCreateCommand": ["npm", "install"]
// Object (named parallel commands)
"postCreateCommand": {
"install-deps": "npm install",
"generate-types": "npm run generate"
}Idempotency
postCreateCommand and postStartCommand may run multiple times across environment rebuilds. Write scripts defensively:
# Check before acting
[ -d node_modules ] || npm install---
Environment Variables
| Property | Scope | Use Case |
|---|---|---|
containerEnv | All container processes | Stable env vars (e.g. NODE_ENV=development) |
remoteEnv | IDE/editor process only | Vars that should not be baked into the image |
{
"containerEnv": { "NODE_ENV": "development" },
"remoteEnv": { "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" }
}Variable substitution — supported in many string properties:
| Variable | Value |
|---|---|
${localWorkspaceFolder} | Host workspace path |
${containerWorkspaceFolder} | In-container workspace path |
${localEnv:MY_VAR} | Value of a host environment variable |
${containerEnv:MY_VAR} | Value of a container environment variable |
---
User and Permissions
{
"remoteUser": "vscode", // User the IDE runs as (non-root recommended)
"containerUser": "root", // User container processes run as (can differ)
"updateRemoteUserUID": true // Sync UID/GID with host user (Linux only; default: true)
}- Always set
remoteUserto a non-root user (vscode,node,python, etc.) updateRemoteUserUID: trueprevents bind-mount permission issues on Linux hosts
---
Networking
{
"forwardPorts": [3000, 5432],
"portsAttributes": {
"3000": { "label": "App", "onAutoForward": "notify" },
"5432": { "label": "Postgres", "onAutoForward": "silent" }
},
"otherPortsAttributes": { "onAutoForward": "ignore" }
}onAutoForward values: "notify", "openBrowser", "openPreview", "silent", "ignore"
---
Mounts
{
"mounts": [
// Named volume for performance (avoids slow bind mounts on macOS/Windows)
"source=node_modules_cache,target=${containerWorkspaceFolder}/node_modules,type=volume",
// Persist bash history
"source=devcontainer-bashhistory,target=/commandhistory,type=volume",
// Additional bind mount
"source=${localWorkspaceFolder}/../shared-lib,target=/shared-lib,type=bind,consistency=cached"
]
}---
VS Code Customizations
{
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-python.python"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"terminal.integrated.defaultProfile.linux": "bash"
}
}
}
}---
Other Useful Properties
| Property | Type | Description |
|---|---|---|
name | string | Display name shown in the IDE |
workspaceFolder | string | In-container path where the workspace is mounted |
workspaceMount | string | Override the default workspace mount |
runArgs | array | Extra docker run arguments (e.g. ["--cap-add=SYS_PTRACE"]) |
shutdownAction | string | "none" or "stopContainer" — what happens when IDE disconnects |
hostRequirements | object | Minimum host CPU, memory, storage, GPU |
privileged | boolean | Run in privileged mode (required for DinD; use with caution) |
capAdd | array | Add Linux capabilities without full privileged (e.g. ["SYS_PTRACE"]) |
securityOpt | array | Security options (e.g. ["seccomp=unconfined"]) |
---
Minimal devcontainer.json Examples
Node.js Project
{
"name": "Node.js App",
"image": "mcr.microsoft.com/devcontainers/node:20",
"customizations": {
"vscode": { "extensions": ["dbaeumer.vscode-eslint"] }
},
"postCreateCommand": "npm install",
"forwardPorts": [3000],
"remoteUser": "node"
}Python Project
{
"name": "Python App",
"image": "mcr.microsoft.com/devcontainers/python:3.12",
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"customizations": {
"vscode": { "extensions": ["ms-python.python", "ms-python.pylint"] }
},
"postCreateCommand": "pip install -r requirements.txt",
"remoteUser": "vscode"
}Go Project
{
"name": "Go App",
"image": "mcr.microsoft.com/devcontainers/go:1.22",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"postCreateCommand": "go mod download",
"forwardPorts": [8080],
"remoteUser": "vscode"
}---
Official Image Registry
Microsoft publishes maintained base images:
| Image | Use Case |
|---|---|
mcr.microsoft.com/devcontainers/base:ubuntu | Generic Ubuntu |
mcr.microsoft.com/devcontainers/python:3.x | Python |
mcr.microsoft.com/devcontainers/node:x | Node.js |
mcr.microsoft.com/devcontainers/go:x.x | Go |
mcr.microsoft.com/devcontainers/rust:latest | Rust |
mcr.microsoft.com/devcontainers/dotnet:8.0 | .NET |
mcr.microsoft.com/devcontainers/java:21 | Java |
All images include common dev tools (git, curl, etc.) and a non-root vscode user.
Dev Container Features and Templates
Reference for consuming Features, authoring custom Features, and distributing Templates.
---
Dev Container Features
Features are self-contained, versioned units of installation code that add tools and configuration to a dev container without modifying the Dockerfile. They are the recommended way to compose development environments.
Consuming Features
Add Features to the features object in devcontainer.json:
{
"features": {
"ghcr.io/devcontainers/features/node:1": { "version": "20" },
"ghcr.io/devcontainers/features/python:1": { "version": "3.12" },
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/azure-cli:1": { "version": "latest" },
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest",
"dockerDashComposeVersion": "v2"
}
}
}Always pin a major version (:1, :2) — never use :latest for production environments. The registry resolves the latest patch within a major version.
Popular Official Features
| Feature | Registry Path | Options |
|---|---|---|
| Node.js | ghcr.io/devcontainers/features/node:1 | version |
| Python | ghcr.io/devcontainers/features/python:1 | version, installTools |
| Go | ghcr.io/devcontainers/features/go:1 | version |
| Rust | ghcr.io/devcontainers/features/rust:1 | version, profile |
| .NET | ghcr.io/devcontainers/features/dotnet:2 | version |
| Java | ghcr.io/devcontainers/features/java:1 | version, jdkDistro |
| Git | ghcr.io/devcontainers/features/git:1 | version, ppa |
| Docker-in-Docker | ghcr.io/devcontainers/features/docker-in-docker:2 | version, moby |
| Docker-outside-Docker | ghcr.io/devcontainers/features/docker-outside-of-docker:1 | |
| kubectl + Helm | ghcr.io/devcontainers/features/kubectl-helm-minikube:1 | version |
| Azure CLI | ghcr.io/devcontainers/features/azure-cli:1 | version |
| AWS CLI | ghcr.io/devcontainers/features/aws-cli:1 | version |
| GitHub CLI | ghcr.io/devcontainers/features/github-cli:1 | version |
Browse the full registry: https://containers.dev/features
Controlling Install Order
Features install in an unspecified order by default. Use overrideFeatureInstallOrder when ordering matters:
{
"features": {
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/myorg/features/my-tool:1": {}
},
"overrideFeatureInstallOrder": [
"ghcr.io/devcontainers/features/node:1",
"ghcr.io/myorg/features/my-tool:1"
]
}Alternatively, within your own Feature's devcontainer-feature.json, use installsAfter to declare soft dependencies.
---
Authoring Custom Features
Structure
my-feature/
├── devcontainer-feature.json # Metadata and options
├── install.sh # Installation script (required)
└── README.md # Optional documentationdevcontainer-feature.json
{
"id": "my-tool",
"version": "1.0.0",
"name": "My Tool",
"description": "Installs My Tool for development",
"documentationURL": "https://github.com/myorg/my-feature",
"licenseURL": "https://github.com/myorg/my-feature/blob/main/LICENSE",
"options": {
"version": {
"type": "string",
"default": "latest",
"description": "Version of My Tool to install",
"proposals": ["latest", "1.0.0", "0.9.0"]
},
"installGlobalTools": {
"type": "boolean",
"default": true,
"description": "Install global CLI tools alongside My Tool"
}
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils"
],
"containerEnv": {
"MY_TOOL_HOME": "/usr/local/my-tool"
}
}install.sh Best Practices
#!/bin/bash
set -e
# Access options (injected as environment variables by CLI)
VERSION="${VERSION:-"latest"}"
INSTALL_GLOBAL_TOOLS="${INSTALLGLOBALTOOLS:-"true"}"
# Detect OS and architecture
. /etc/os-release
ARCH="$(uname -m)"
case "${ARCH}" in
x86_64) ARCH="amd64" ;;
aarch64 | arm64) ARCH="arm64" ;;
*) echo "Unsupported architecture: ${ARCH}"; exit 1 ;;
esac
# Install dependencies
apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates
# Resolve 'latest' version
if [ "${VERSION}" = "latest" ]; then
VERSION=$(curl -sf "https://api.github.com/repos/myorg/my-tool/releases/latest" \
| grep '"tag_name"' | sed -E 's/"tag_name": "v?([^"]+)".*/\1/')
fi
# Download and install
curl -fsSL "https://example.com/releases/${VERSION}/my-tool-${ARCH}" \
-o /usr/local/bin/my-tool
chmod +x /usr/local/bin/my-tool
# Respect remoteUser (CLI provides _REMOTE_USER variable)
if [ "${INSTALL_GLOBAL_TOOLS}" = "true" ]; then
su "${_REMOTE_USER}" -c "my-tool install-globals"
fi
echo "Done! Installed my-tool ${VERSION}"Key Authoring Rules
1. Idempotency — scripts may run multiple times; check before acting 2. OS/arch detection — handle amd64 and arm64 at minimum; detect distro via /etc/os-release 3. Respect `_REMOTE_USER` — use the _REMOTE_USER variable (injected by CLI) when installing user-scoped tools 4. Clean up after `apt-get` — add && rm -rf /var/lib/apt/lists/* to keep layers small 5. Export env vars — use containerEnv in metadata rather than sourcing .bashrc (more reliable across shells)
Testing Features
Use the official test framework via the Dev Container CLI:
# Install CLI
npm install -g @devcontainers/cli
# Test against a specific base image
devcontainer features test \
--features my-feature \
--base-image mcr.microsoft.com/devcontainers/base:ubuntu \
.
# Test against multiple images (define in devcontainer-feature.json's "scenarios")
devcontainer features test .Publishing Features
# Build and push to GitHub Container Registry
devcontainer features publish \
--registry ghcr.io \
--namespace myorg \
./my-featureThe Feature becomes available at ghcr.io/myorg/features/my-feature:1.
---
Dev Container Templates
Templates are complete devcontainer.json configurations (plus optional Dockerfiles and Compose files) that define standardised starting points for new projects.
Using a Template
Via VS Code: Dev Containers: Add Dev Container Configuration Files → browse Templates
Via CLI:
devcontainer templates apply \
--template-id ghcr.io/devcontainers/templates/python:1 \
--workspace-folder .Popular Official Templates
| Template | ID |
|---|---|
| Python 3 | ghcr.io/devcontainers/templates/python:1 |
| Node.js | ghcr.io/devcontainers/templates/javascript-node:1 |
| Go | ghcr.io/devcontainers/templates/go:1 |
| Rust | ghcr.io/devcontainers/templates/rust:1 |
| .NET | ghcr.io/devcontainers/templates/dotnet:1 |
| Java | ghcr.io/devcontainers/templates/java:1 |
Browse the full registry: https://containers.dev/templates
Authoring Custom Templates
my-template/
├── devcontainer-template.json # Metadata
├── .devcontainer/
│ ├── devcontainer.json # The template configuration
│ └── Dockerfile # (optional)
└── README.md`devcontainer-template.json`:
{
"id": "my-stack",
"version": "1.0.0",
"name": "My Stack",
"description": "Development environment for My Stack projects",
"documentationURL": "https://github.com/myorg/my-template",
"licenseURL": "https://github.com/myorg/my-template/blob/main/LICENSE",
"options": {
"nodeVersion": {
"type": "string",
"default": "20",
"description": "Node.js version",
"proposals": ["20", "18", "16"]
}
},
"platforms": ["linux/amd64", "linux/arm64"],
"publisher": "myorg",
"keywords": ["node", "typescript", "my-stack"]
}Template Distribution
1. Publish to GitHub Container Registry as OCI artifacts (tarballs) 2. Add devcontainer-collection.json to the repository root for discovery 3. Use semantic versioning: 1.0.0, 1.1.0, etc.
devcontainer templates publish \
--registry ghcr.io \
--namespace myorg \
./my-templateCentralised Team Repository Pattern
Host a team-devcontainers repository with:
team-devcontainers/
├── devcontainer-collection.json # Metadata index
├── templates/
│ ├── backend-service/ # Java Spring Boot template
│ ├── frontend-app/ # React/TypeScript template
│ └── data-science/ # Python + Jupyter template
└── features/
├── internal-cli/ # Company CLI tool
└── vpn-config/ # Internal network setupShare the collection URL with the team; point the VS Code Add Config dialog to it.
Performance and Security for Dev Containers
Optimising build speed, startup time, disk I/O, and hardening container security.
---
Performance: Dockerfile Layer Caching
Layer Ordering Principle
Docker rebuilds every layer after the first changed layer. Order instructions from least- to most-frequently changed:
# ✅ Good — stable layers first
FROM mcr.microsoft.com/devcontainers/base:ubuntu
# 1. System packages (rarely change)
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 2. Package manager dependencies (change occasionally)
COPY package.json package-lock.json ./
RUN npm ci
# 3. Source code (changes frequently)
COPY . .
RUN npm run build# ❌ Bad — copying source early invalidates dependency cache
FROM node:20
COPY . . # Any source change invalidates everything below
RUN npm ci # Always re-runsBuildKit Cache Mounts
Persist package manager caches between builds even when layers are invalidated:
# Node.js / npm
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Python / pip
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Go modules
RUN --mount=type=cache,target=/root/go/pkg/mod \
go mod download
# Rust / cargo
RUN --mount=type=cache,target=/root/.cargo/registry \
cargo build --release
# apt packages
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y curl wgetMulti-Stage Builds
Separate build and runtime stages to reduce final image size and improve cacheability:
# Stage 1 — build dependencies (cacheable)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Stage 2 — build application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3 — minimal runtime image
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]In devcontainer.json, target the dev-friendly stage:
{
"build": {
"dockerfile": "Dockerfile",
"target": "builder" // Use the full build stage for development
}
}Minimise Build Context
Create .dockerignore:
.git
node_modules
dist
build
.DS_Store
*.log
.env
.env.*
coverage
__pycache__
*.pyc
.pytest_cache---
Performance: Pre-building Images in CI
Pre-build the dev container image in CI and push to a registry. Developers pull a ready-to-use image instead of building locally.
GitHub Actions Workflow
# .github/workflows/devcontainer-prebuild.yml
name: Pre-build Dev Container
on:
push:
branches: [main]
paths:
- '.devcontainer/**'
- 'package.json'
- 'requirements.txt'
schedule:
- cron: '0 3 * * 1' # Weekly on Mondays to pick up Feature updates
jobs:
prebuild:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pre-build and push
uses: devcontainers/ci@v0.3
with:
imageName: ghcr.io/${{ github.repository }}/devcontainer
cacheFrom: ghcr.io/${{ github.repository }}/devcontainer
push: alwaysReference Pre-built Image
// .devcontainer/devcontainer.json
{
"image": "ghcr.io/myorg/my-repo/devcontainer:latest",
// Features/customizations still apply on top of the pre-built image
"customizations": {
"vscode": { "extensions": ["dbaeumer.vscode-eslint"] }
},
"postCreateCommand": "npm install"
}Embedding Metadata in the Image
When using devcontainers/ci, metadata (extensions, settings, lifecycle scripts) is automatically embedded as image labels. The image becomes self-contained — tools apply settings automatically when the image is pulled.
---
Performance: Disk I/O
Named Volumes for Heavy Directories
On macOS and Windows, bind mounts pass through a virtualisation layer and are slow. Use named volumes for directories with heavy I/O:
{
"mounts": [
// node_modules in a named volume — native Linux filesystem speed
"source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
// Python virtualenv
"source=${localWorkspaceFolderBasename}-venv,target=${containerWorkspaceFolder}/.venv,type=volume",
// Cargo build cache
"source=cargo-cache,target=/usr/local/cargo/registry,type=volume"
]
}Trade-off: Named volumes are not visible on the host filesystem. They persist across container rebuilds but must be explicitly deleted when resetting.
Virtio-fs (Docker Desktop macOS/Windows)
Enable in Docker Desktop → Settings → General → VirtioFS (Virtual File Sharing). Provides up to 2–10× faster bind mount performance vs default gRPC-FUSE.
Requires Docker Desktop 4.6+ on macOS.
Synchronized File Shares (Docker Desktop 4.27+)
Docker Desktop's Synchronized File Shares feature maintains a synchronised copy of host files inside the VM, providing near-native speeds for bind mounts. Enable per-mount in Docker Desktop settings.
---
Security: Non-Root Users
Always Use remoteUser
{
"remoteUser": "vscode" // The user VS Code and terminals run as
}The mcr.microsoft.com/devcontainers/* images include a pre-configured vscode user. Other images may use node, python, app, etc.
Never develop as `root` — even inside a container, running as root exposes the host if there are container escape vulnerabilities.
UID/GID Mapping (Linux Hosts)
On Linux, the container user's UID must match the host user's UID to avoid bind mount permission errors:
{
"remoteUser": "vscode",
"updateRemoteUserUID": true // Default: true — automatically syncs UID/GID
}If you see files owned by root after the container writes to a bind-mounted directory, ensure updateRemoteUserUID is true.
Running as a Specific UID
# Explicitly set UID in Dockerfile
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN groupmod --gid $USER_GID vscode \
&& usermod --uid $USER_UID --gid $USER_GID vscode---
Security: Secrets Management
Never Bake Secrets Into Images
# ❌ NEVER do this
ENV GITHUB_TOKEN=ghp_xxx # Visible in image history
RUN curl -H "Authorization: token $GITHUB_TOKEN" ...BuildKit Secret Mounts (Build-Time Secrets)
# Dockerfile — secret is available only during this RUN; not in the image
RUN --mount=type=secret,id=github_token \
GITHUB_TOKEN=$(cat /run/secrets/github_token) \
npm install --registry https://npm.pkg.github.com# Build with the secret
docker build --secret id=github_token,src=$HOME/.github_token .SSH Agent Forwarding (Git Credentials)
The Dev Containers extension automatically forwards the local SSH agent socket into the container. Prerequisites:
# On the host, ensure SSH agent is running and key is loaded
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# Verify
ssh-add -lInside the container, Git operations over SSH work automatically.
GPG Signing
# On the host
gpg --list-secret-keys
# Export the key ID you want to use
echo "export GPG_KEY_ID=<your-key-id>" >> ~/.bashrc// devcontainer.json — mount GPG socket
{
"mounts": [
"source=${localEnv:HOME}/.gnupg,target=/home/vscode/.gnupg,type=bind,consistency=cached"
],
"postCreateCommand": "git config --global user.signingkey $GPG_KEY_ID && git config --global commit.gpgsign true"
}Environment Variable Secrets
// devcontainer.json — inject from host environment (never hardcode values)
{
"remoteEnv": {
"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
"AWS_ACCESS_KEY_ID": "${localEnv:AWS_ACCESS_KEY_ID}",
"AWS_SECRET_ACCESS_KEY": "${localEnv:AWS_SECRET_ACCESS_KEY}"
}
}For secrets that vary per developer, use a .env file (added to .gitignore) and reference it:
{
"runArgs": ["--env-file", "${localWorkspaceFolder}/.env"]
}Docker BuildKit Inline Cache for CI
# docker-compose.dev.yml — enable BuildKit inline cache
services:
app:
build:
cache_from:
- ghcr.io/myorg/my-repo/devcontainer:latest# Build with registry cache backend (caches all stages)
docker buildx build \
--cache-from type=registry,ref=ghcr.io/myorg/cache \
--cache-to type=registry,ref=ghcr.io/myorg/cache,mode=max \
.---
Security: Hardened Base Images
- Use Docker's official hardened images (reduced attack surface, signed, regularly patched)
- Review the SBOM (Software Bill of Materials) for known vulnerabilities
- Pin base image digests for full reproducibility:
# Pin by digest instead of tag for maximum reproducibility
FROM mcr.microsoft.com/devcontainers/base@sha256:abc123...---
Security: Capability Management
Avoid "privileged": true unless absolutely required (e.g. Docker-in-Docker). Instead, grant specific capabilities:
{
"capAdd": ["SYS_PTRACE"], // Enable debuggers (ptrace)
"securityOpt": ["seccomp=unconfined"] // Disable seccomp for debugging
}Common capabilities for development:
| Capability | Use Case |
|---|---|
SYS_PTRACE | Debuggers (GDB, strace) |
NET_ADMIN | Network testing tools |
SYS_ADMIN | FUSE mounts |
---
CI/CD Integration Checklist
- [ ] Pre-build image on
mainbranch changes and weekly schedule - [ ] Push to a container registry with appropriate access controls
- [ ] Use
--cache-fromto reuse previous build layers - [ ] Embed devcontainer metadata in image labels (
devcontainers/cidoes this automatically) - [ ] Scan image for vulnerabilities in CI (Trivy, Snyk, Docker Scout)
- [ ] Rotate base image regularly to pick up OS patches
Dev Container Troubleshooting
Common issues and fixes for Dev Container configuration, performance, and credential problems.
---
Permission Errors (UID/GID Mismatch)
Symptom
Files created inside the container appear owned by root on the host, or you get Permission denied when the container tries to write to a bind-mounted directory.
Cause
The container user's UID differs from the host user's UID.
Fix
{
"remoteUser": "vscode",
"updateRemoteUserUID": true // Default true — re-maps UID/GID to match host
}If you have a custom Dockerfile and the user UID is hardcoded:
ARG USER_UID=1000
ARG USER_GID=1000
RUN usermod -u $USER_UID vscode && groupmod -g $USER_GID vscodePass the host user's UID at build time:
{
"build": {
"dockerfile": "Dockerfile",
"args": {
"USER_UID": "1000",
"USER_GID": "1000"
}
}
}---
Slow File I/O (macOS / Windows)
Symptom
npm install, pip install, or file-watching tools (webpack, nodemon) are extremely slow inside the container.
Fixes — in order of impact
1. Use named volumes for heavy directories
{
"mounts": [
"source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
]
}2. Enable Virtio-fs in Docker Desktop
Docker Desktop → Settings → General → Virtual File Sharing → VirtioFS
Requires Docker Desktop 4.6+ on macOS.
3. Enable Synchronized File Shares
Docker Desktop → Settings → Resources → File Sharing → enable Synchronized file shares for your project directory.
4. Store the project inside WSL2 (Windows only)
Do not clone the repository to C:\Users\.... Clone inside WSL2:
# Inside WSL2 terminal
git clone git@github.com:myorg/my-repo.git ~/projects/my-repo
# Then open VS Code from WSL2: code ~/projects/my-repo---
SSH Credentials Not Available Inside Container
Symptom
git clone git@github.com:... or git fetch fails with Permission denied (publickey).
Fix
Ensure the SSH agent is running and your key is loaded on the host:
# macOS / Linux
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519 # or id_rsa
# Verify
ssh-add -lThe Dev Containers extension automatically forwards the SSH agent socket. If it still fails:
1. Check SSH_AUTH_SOCK is set on the host: echo $SSH_AUTH_SOCK 2. Rebuild the container (Command Palette → Dev Containers: Rebuild Container) 3. Inside container, verify: ssh-add -l
macOS Keychain Integration
# ~/.ssh/config — persist keys across reboots on macOS
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519---
GPG Signing Fails Inside Container
Symptom
git commit fails with error: gpg failed to sign the data.
Fix
# On the host
export GPG_TTY=$(tty)
gpgconf --launch gpg-agentMount the GPG socket into the container:
{
"mounts": [
"source=${localEnv:HOME}/.gnupg,target=/home/vscode/.gnupg,type=bind,consistency=cached"
],
"postCreateCommand": "gpg --list-keys && git config --global gpg.program gpg2"
}Ensure gnupg2 is installed in the container:
{
"features": {
"ghcr.io/devcontainers/features/git:1": {}
},
"postCreateCommand": "sudo apt-get install -y gnupg2"
}---
Container Fails to Start / Builds Hang
Symptom
The container never starts, or the build hangs indefinitely at a RUN command.
Diagnosis
Open the Dev Containers: Show Container Log output (Command Palette) to see the full build output.
Common Causes
| Cause | Fix |
|---|---|
| Network timeout pulling image | Check Docker network; try docker pull <image> manually |
postCreateCommand hangs | Add a timeout or run interactively first |
| Feature install fails | Check the Feature's GitHub issues; pin to a different version |
| Dockerfile syntax error | Run docker build . manually from .devcontainer/ |
| Out of disk space | Run docker system prune to free space |
Force Rebuild
Command Palette → Dev Containers: Rebuild Container Without CacheOr from the CLI:
devcontainer up --remove-existing-container --workspace-folder .---
postCreateCommand Fails or Runs Repeatedly
Symptom
Dependency installation fails, or runs every time the container starts (instead of just once).
Cause
postCreateCommand should run only once after container creation, but a container rebuild triggers it again. If the command is not idempotent, it may fail on re-run.
Fix — Make Commands Idempotent
# ❌ Fails on re-run if .env already exists
cp .env.example .env
# ✅ Safe to run multiple times
[ -f .env ] || cp .env.example .env
# ✅ npm install is already idempotent
npm install
# ✅ Check before running migration
python manage.py migrate --check || python manage.py migrateSeparate One-Time vs. Every-Start Commands
{
"postCreateCommand": "npm install && cp -n .env.example .env", // Once
"postStartCommand": "npm run dev:services" // Every start
}---
Docker-in-Docker Issues
Symptom: Cannot connect to the Docker daemon
Error: Cannot connect to the Docker daemon at unix:///var/run/docker.sockFix: Ensure you have added the DinD/DooD Feature and the container is privileged (for DinD):
{
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"privileged": true
}Rebuild the container after adding these settings.
Symptom: devcontainer with docker-in-docker doesn't start (Known Issue)
This is a known issue (GitHub: devcontainer/cli#831). Workarounds:
1. Downgrade the docker-in-docker Feature to :1 (older, stable version) 2. Switch to docker-outside-of-docker instead 3. Use "overrideCommand": false in devcontainer.json
---
Extension Not Installed in Container
Symptom
A VS Code extension works locally but is missing inside the container.
Fix
Add it to customizations.vscode.extensions in devcontainer.json:
{
"customizations": {
"vscode": {
"extensions": ["ms-python.python"]
}
}
}Then rebuild: Dev Containers: Rebuild Container.
Note: Extensions installed manually in the container are not persisted across rebuilds. Always declare them in devcontainer.json.
---
Port Not Accessible on Localhost
Symptom
The app is running inside the container but http://localhost:3000 is not reachable.
Fix
{
"forwardPorts": [3000]
}If the port still does not appear forwarded, check the VS Code Ports panel (View → Open View → Ports) and forward it manually.
Ensure the app is listening on 0.0.0.0 (all interfaces), not just 127.0.0.1:
// ❌ Only accessible inside container
app.listen(3000, '127.0.0.1', ...)
// ✅ Accessible via port forwarding
app.listen(3000, '0.0.0.0', ...)---
Debugging Lifecycle Script Execution
Check which lifecycle scripts ran and their output:
# Inside the container — Dev Container CLI stores logs here
cat ~/.devcontainer-init.log 2>/dev/null || echo "No log found"
# Or check VS Code Dev Containers output panel:
# View → Output → Dev ContainersExecution order reference:
Host: initializeCommand
Container: onCreateCommand → updateContentCommand → postCreateCommand
Each start: postStartCommand
Each attach: postAttachCommand---
Cleaning Up Stale Resources
# Remove all stopped dev containers
docker container prune
# Remove unused named volumes (⚠️ removes persisted data)
docker volume prune
# Remove dangling images
docker image prune
# Full cleanup (⚠️ removes everything not currently in use)
docker system prune --volumes
# Remove a specific named volume
docker volume rm my-repo-node_modules