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
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.
- "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
git clone → Reopen 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.
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
→
→
→
→
→
Three Ways to Define a Container
| Approach | Config | Best For | Trade-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.
| Situation | Use 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 |
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
# 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.
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
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.
{
// ─── 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
| Field | Type | What it does |
name | string | Display name shown in VS Code status bar |
image | string | Docker image to use directly (no Dockerfile needed) |
build | object | Build from a local Dockerfile; supports args and target |
dockerComposeFile | string/array | Path to Docker Compose file(s); requires service |
features | object | Install Dev Container Features (tools) on top of any base image |
forwardPorts | array | Ports to forward from container to host automatically |
customizations.vscode.extensions | array | VS Code extension IDs installed inside the container |
customizations.vscode.settings | object | VS Code settings applied inside the container |
postCreateCommand | string/array | Runs once after container is created (install deps, seed db, etc.) |
remoteUser | string | User VS Code runs as inside the container (use non-root for security) |
remoteEnv | object | Environment variables set for VS Code and terminal sessions |
mounts | array | Additional bind mounts or named volumes |
runArgs | array | Extra docker run arguments (capabilities, security opts) |
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)
{
"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)
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
{
"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"
}
#!/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
{
"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
}
]
}
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.
{
"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)
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
{
"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.
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
}
}
})
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
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:
{
"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"
}
#!/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!"
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.
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
{
"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"
}
#!/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."
{
"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"
}
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:
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.
{
"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.
| Feature | OCI Reference | Use Case |
| Git | ghcr.io/devcontainers/features/git:1 | Latest git, always |
| GitHub CLI | ghcr.io/devcontainers/features/github-cli:1 | PR, release management in terminal |
| Node.js | ghcr.io/devcontainers/features/node:1 | Add Node to any non-Node image |
| Docker-in-Docker | ghcr.io/devcontainers/features/docker-in-docker:2 | Build images, run Compose inside the container |
| kubectl + Helm | ghcr.io/devcontainers/features/kubectl-helm-minikube:1 | Kubernetes development |
| AWS CLI | ghcr.io/devcontainers/features/aws-cli:1 | AWS deployments from the container |
| Terraform | ghcr.io/devcontainers/features/terraform:1 | IaC work alongside your app |
| common-utils | ghcr.io/devcontainers/features/common-utils:2 | zsh, oh-my-zsh, useful CLI tools |
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.
| Hook | Runs When | Where | Best 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 |
{
// 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.
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)
{
// 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"
}
}
# .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_...
# 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
{
"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.
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.
{
"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)
{
// 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.
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.
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
{
"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.
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
{
"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.
| Problem | Likely Cause | Fix |
| 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
# 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
VS Code Command Palette Actions
| Command | What it does |
Dev Containers: Reopen in Container | Start the dev container (or switch to it) |
Dev Containers: Rebuild Container | Rebuild the image and restart (uses cache) |
Dev Containers: Rebuild Container Without Cache | Full clean rebuild — use after changing Dockerfile |
Dev Containers: Reopen Folder Locally | Exit the container, return to local workspace |
Dev Containers: Open Container Configuration File | Jump to devcontainer.json |
Dev Containers: Show Container Log | See build output, lifecycle script output |
Dev Containers: Clone Repository in Container Volume | Clone a repo into a Docker volume (maximum performance) |
Dev Containers: Add Dev Container Configuration Files | Scaffold 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
| Stack | Image |
| Python 3.12 | mcr.microsoft.com/devcontainers/python:3.12 |
| Python 3.11 | mcr.microsoft.com/devcontainers/python:3.11 |
| Node 22 + TypeScript | mcr.microsoft.com/devcontainers/typescript-node:22 |
| Node 20 | mcr.microsoft.com/devcontainers/javascript-node:20 |
| Go 1.22 | mcr.microsoft.com/devcontainers/go:1.22 |
| Rust | mcr.microsoft.com/devcontainers/rust:latest |
| Java 21 | mcr.microsoft.com/devcontainers/java:21 |
| Ubuntu 24.04 | mcr.microsoft.com/devcontainers/base:ubuntu-24.04 |
| Debian Bookworm | mcr.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.