VSCode with DevContainers: Building Secure, Isolated and Reproducible Development Environments
I'd like to share my observations and insights about pair programming with AI agents.
Currently, I'm involved in several projects with different technology stacks. For example, over the past few months, I've been working on three different projects with three distinct technology stacks:
- A Python Stack project developing an AI agent using LangChain, LangGraph, LangSmith, and other Python libraries
- A JavaScript Stack project developing a web UI to interact with an AI agent
- A Java Stack project developing a backend that exposes REST APIs for AI agents
P.S. I agree this sounds like a zoo of technologies. However, this decision was made deliberately to have the flexibility to choose the best tool from each ecosystem for specific use cases.
To maintain flexibility across these projects, I need multiple libraries and tools installed on my local system:
- All required Python ecosystem tools (e.g., pip, venv, libraries)
- All required JavaScript and Node.js tools (e.g., npm, nvm, libraries)
- All required Java tools (e.g., Maven, Gradle, JDK)
- Docker with docker-compose
- VSCode with various extensions
- kubectl with access to multiple Kubernetes clusters
- Kind for creating local Kubernetes clusters for testing
- Helm for managing Kubernetes charts
- Terraform for infrastructure as code
- And more...
The Problem
Having all these tools installed on my local system is uncomfortable and risky. It can cause:
- Security issues affecting both my local system and the projects themselves
- Reproducibility challenges
- AI Agent safety concerns
- Version conflicts
- Broken dependencies
I'm also constantly cloning GitHub repositories to test different technologies and tools. This poses security risks if any repository contains malicious code or vulnerabilities.
Additionally, switching between projects with different technology stacks requires manual environment changes. Sharing my development environment with colleagues or the community becomes a headache.
The Solution: VSCode with DevContainers
I've decided to use VSCode with DevContainers to create secure, isolated and reproducible development environments for each project. With DevContainers, I can define a complete development environment that:
- With all required tools and dependencies pre-installed
- Clones necessary repositories
- Installs all desired tools and dependencies
- Executes everything needed for testing without risking my local system
Pros and Cons
Cons
- Higher resource consumption: Running containers in the background consumes more CPU, RAM, and disk space
- Initial setup time: Projects with many dependencies may take time to build initially (though subsequent builds are faster thanks to Docker layer caching). Rebuilding after modifying
devcontainer.jsonalso requires time
Pros
- Security: Working in a container reduces the risk of affecting your local system or introducing vulnerabilities, protecting both your local environment and development projects
- Isolation: Each project is encapsulated in its own environment with everything it needs
- Reproducibility: Easy to share development environments with others, ensuring everyone works with the same tools and configurations
- AI Agent Safety: Running AI agents inside DevContainers allows you to grant necessary permissions without worrying about local system security. In my case, I've restricted AI agent interactions with git and MCP tools that execute commands capable of making external system modifications
DevContainer Configuration
Let me share my DevContainer configuration for web UI development.
VSCode Configuration Strategy
Before diving into the configuration files, it's important to understand my approach to VSCode settings and extensions. VSCode has different configuration levels:
- User settings: Global settings that apply to all projects
- Workspace settings: Settings specific to a workspace/project
- DevContainer settings: Settings that only apply within the container
In my case, I keep both user settings and workspace settings empty. All my configuration lives at the DevContainer level. This ensures complete isolation and reproducibility.
Similarly, for extensions, I only have two extensions installed locally:
ms-vscode-remote.remote-containers- Required for DevContainer functionalitygithub.github-vscode-theme- Personal theme preference shared across all DevContainers
All other extensions are installed at the DevContainer level. This means each project gets exactly the extensions it needs, without cluttering my local VSCode installation or creating conflicts between projects.
Folder Structure
.devcontainer/
โโโ devcontainer.json # Main configuration file
โโโ post-create.sh # Post-create script to set up the environment
โโโ post-start.sh # Post-start script that runs when the container starts
devcontainer.json
The devcontainer.json file is the main configuration file for the DevContainer, where we define all container settings:
{
"name": "BIO-LINK",
"image": "mcr.microsoft.com/devcontainers/javascript-node:22",
"containerEnv": {},
"features": {
// https://containers.dev/features
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": true,
"installOhMyZsh": true,
"installOhMyZshConfig": true,
"upgradePackages": true,
"username": "devcontainer",
"userUid": "automatic",
"userGid": "automatic"
}
},
"containerUser": "devcontainer",
// "updateRemoteUserUID": true,
"postCreateCommand": "chmod +x ./.devcontainer/post-create.sh && ./.devcontainer/post-create.sh",
"postStartCommand": "chmod +x ./.devcontainer/post-start.sh && ./.devcontainer/post-start.sh",
"customizations": {
"vscode": {
"settings": {
"window.zoomLevel": 0.5,
"window.title": "${rootName}",
"window.autoDetectColorScheme": true,
"workbench.sideBar.location": "left",
"workbench.editor.showTabs": "none",
"workbench.tree.indent": 24,
"workbench.preferredLightColorTheme": "GitHub Light",
"workbench.preferredDarkColorTheme": "GitHub Dark",
"editor.minimap.enabled": false,
"editor.scrollbar.vertical": "hidden",
"editor.overviewRulerBorder": false,
"editor.hideCursorInOverviewRuler": true,
"editor.cursorSmoothCaretAnimation": "on",
"editor.wordWrap": "on",
"editor.formatOnSave": true,
"editor.cursorBlinking": "phase",
"editor.linkedEditing": true,
"editor.guides.bracketPairs": true,
"editor.foldingStrategy": "indentation",
"editor.foldingImportsByDefault": true,
"editor.foldingMethodsByDefault": true,
"terminal.integrated.defaultProfile.osx": "zsh",
"terminal.integrated.defaultProfile.linux": "zsh",
"breadcrumbs.enabled": false,
"git.confirmSync": false,
"git.autofetch": true,
"github.copilot.enable": {
"*": true,
"markdown": true,
// "scminput": false,
"plaintext": true
},
"github.copilot.nextEditSuggestions.enabled": true,
"diffEditor.codeLens": true,
"files.autoSave": "afterDelay",
"files.autoSaveDelay": 1000,
"explorer.confirmDelete": false,
"explorer.confirmDragAndDrop": false,
"outline.collapseItems": "alwaysCollapse",
"chat.mcp.gallery.enabled": true,
// "debug.javascript.autoAttachFilter": "disabled",
"chat.promptFilesRecommendations": {
"speckit.constitution": true,
"speckit.specify": true,
"speckit.plan": true,
"speckit.tasks": true,
"speckit.implement": true
},
"chat.tools.terminal.autoApprove": {
"cd": true,
"echo": true,
"ls": true,
"pwd": true,
"cat": true,
"head": true,
"tail": true,
"findstr": true,
"wc": true,
"tr": true,
"cut": true,
"cmp": true,
"which": true,
"basename": true,
"dirname": true,
"realpath": true,
"readlink": true,
"stat": true,
"file": true,
"du": true,
"df": true,
"sleep": true,
"grep": true,
"git status": true,
"git log": true,
"git show": true,
"git diff": true,
"git grep": true,
"git rev-parse": true,
"Get-ChildItem": true,
"Get-Content": true,
"Get-Date": true,
"Get-Random": true,
"Get-Location": true,
"Write-Host": true,
"Write-Output": true,
"Split-Path": true,
"Join-Path": true,
"Start-Sleep": true,
"Where-Object": true,
"/^Select-[a-z0-9]/i": true,
"/^Measure-[a-z0-9]/i": true,
"/^Compare-[a-z0-9]/i": true,
"/^Format-[a-z0-9]/i": true,
"/^Sort-[a-z0-9]/i": true,
"column": true,
"/^column\\b.*-c\\s+[0-9]{4,}/": true,
"date": true,
"/^date\\b.*(-s|--set)\\b/": true,
"find": true,
"/^find\\b.*-(delete|exec|execdir|fprint|fprintf|fls|ok|okdir)\\b/": true,
"sort": true,
"/^sort\\b.*-(o|S)\\b/": true,
"tree": true,
"/^tree\\b.*-o\\b/": true,
"/\\(.+\\)/s": {
"approve": true,
"matchCommandLine": true
},
"/\\{.+\\}/s": {
"approve": true,
"matchCommandLine": true
},
"/`.+`/s": {
"approve": true,
"matchCommandLine": true
},
"rm": true,
"rmdir": true,
"del": true,
"Remove-Item": true,
"ri": true,
"rd": true,
"erase": true,
"dd": true,
"kill": true,
"ps": true,
"top": true,
"Stop-Process": true,
"spps": true,
"taskkill": true,
"taskkill.exe": true,
"curl": true,
"wget": true,
"Invoke-RestMethod": true,
"Invoke-WebRequest": true,
"irm": true,
"iwr": true,
"chmod": true,
"chown": true,
"sp": true,
"Set-Acl": true,
"jq": true,
"xargs": true,
"eval": true,
"Invoke-Expression": true,
"iex": true,
"bash .specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks": {
"approve": true,
"matchCommandLine": true
},
"/dev/null": true,
"npm": {
"approve": true,
"matchCommandLine": true
},
"npx": {
"approve": true,
"matchCommandLine": true
},
"sed": true,
".specify/scripts/bash/": true,
".specify/scripts/powershell/": true,
"claude": true
}
},
"extensions": [
// GitHub Copilot
"github.copilot",
"github.copilot-chat",
// Claude Code
// "anthropic.claude-code"
// Codex
// "openai.chatgpt",
// Gemini
// "google.geminicodeassist",
// Kilo Code
// "kilocode.Kilo-Code",
// Cline
// "saoudrizwan.claude-dev",
// ESLint
"dbaeumer.vscode-eslint",
"mhutchie.git-graph",
"anweber.reveal-button",
"chrisdias.promptboost"
]
}
}
}
post-create.sh
The post-create.sh script runs once after the container is created. This is where you install additional dependencies and configure the environment:
#!/bin/bash
# Exit immediately on error, treat unset variables as an error, and fail if any command in a pipeline fails.
set -euo pipefail
# Function to run a command and show logs only on error
run_command() {
local command_to_run="$*"
local output
local exit_code
# Capture all output (stdout and stderr)
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
exit_code=${exit_code:-0}
if [ $exit_code -ne 0 ]; then
echo -e "\033[0;31m[ERROR] Command failed (Exit Code $exit_code): $command_to_run\033[0m" >&2
echo -e "\033[0;31m$output\033[0m" >&2
exit $exit_code
fi
}
echo -e "\n๐ค Installing project dependencies..."
run_command "npm install"
echo "โ
Done"
echo -e "\n๐ค Installing Claude CLI..."
run_command "npm install -g @anthropic-ai/claude-code@latest"
# run_command "claude --dangerously-skip-permissions"
echo "โ
Done"
echo -e "\n๐ค Installing Claude Flow CLI..."
run_command "npm install -g claude-flow@alpha"
echo "โ
Done"
# echo -e "\n๐ค Installing Agentic Flow..."
# run_command "npm i agentic-flow"
# echo "โ
Done"
# echo -e "\n๐ค Installing Copilot CLI..."
# run_command "npm install -g @github/copilot@latest"
# echo "โ
Done"
# echo -e "\n๐ค Installing Codex CLI..."
# run_command "npm install -g @openai/codex@latest"
# echo "โ
Done"
# echo -e "\n๐ค Installing Gemini CLI..."
# run_command "npm install -g @google/gemini-cli@latest"
# echo "โ
Done"
# echo -e "\n๐ค Installing Ollama CLI..."
# run_command "npm install -g ollama@latest"
# echo "โ
Done"
echo -e "\n๐ค Installing zsh-autosuggestions..."
run_command "git clone https://github.com/zsh-users/zsh-autosuggestions ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions"
# Enable the plugin in .zshrc
if grep -q "^plugins=(git)$" ~/.zshrc; then
sed -i 's/^plugins=(git)$/plugins=(git zsh-autosuggestions)/' ~/.zshrc
fi
echo "โ
Done"
echo -e "\n๐ค Installing zsh-syntax-highlighting..."
run_command "git clone https://github.com/zsh-users/zsh-syntax-highlighting ~/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting"
# Enable the plugin in .zshrc
if grep -q "^plugins=(git zsh-autosuggestions)$" ~/.zshrc; then
sed -i 's/^plugins=(git zsh-autosuggestions)$/plugins=(git zsh-autosuggestions zsh-syntax-highlighting)/' ~/.zshrc
fi
echo "โ
Done"
echo -e "\n๐งน Cleaning cache..."
run_command "sudo apt-get autoclean"
run_command "sudo apt-get clean"
echo "โ
Setup completed. Happy coding! ๐"
post-start.sh
The post-start.sh script runs every time the container starts. This is where you execute commands needed on each startup:
#!/bin/bash
# Exit immediately on error, treat unset variables as an error, and fail if any command in a pipeline fails.
set -euo pipefail
# Function to run a command and show logs only on error
run_command() {
local command_to_run="$*"
local output
local exit_code
# Capture all output (stdout and stderr)
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
exit_code=${exit_code:-0}
if [ $exit_code -ne 0 ]; then
echo -e "\033[0;31m[ERROR] Command failed (Exit Code $exit_code): $command_to_run\033[0m" >&2
echo -e "\033[0;31m$output\033[0m" >&2
exit $exit_code
fi
}
# echo -e "\n๐ค Configuring Git safe directory..."
# run_command "git config --global --add safe.directory ${containerWorkspaceFolder}"
# echo "โ
Done"
echo -e "\n๐ค Setting up Claude CLI alias..."
# Add alias to .bashrc
if ! grep -q "alias claude=" ~/.bashrc 2>/dev/null; then
echo 'alias claude="claude --dangerously-skip-permissions"' >> ~/.bashrc
fi
# Add alias to .zshrc (for zsh shell)
if ! grep -q "alias claude=" ~/.zshrc 2>/dev/null; then
echo 'alias claude="claude --dangerously-skip-permissions"' >> ~/.zshrc
fi
echo "โ
Done"
echo -e "\n๐ค Setting zsh as default shell..."
# Change default shell to zsh for the current user
if [ "$SHELL" != "$(which zsh)" ]; then
run_command "sudo chsh -s $(which zsh) $(whoami)"
fi
echo "โ
Done"
echo -e "\nโ
Container started and ready!"
Taking It Further: GitHub Codespaces
You can evolve this approach even further by using GitHub Codespaces to run DevContainers in the cloud. This provides an additional layer of protection for your local machine.
However, this approach has additional costs since GitHub Codespaces isn't free. Pricing varies depending on the virtual machine configuration you choose for your Codespace.
Conclusion
Using VSCode with DevContainers for pair programming with AI agents is an excellent way to maintain security, isolation, and reproducibility in your development environments. It provides peace of mind when working with AI-powered tools while keeping your local system protected.
Resources and Further Reading
- Official Documentation: containers.dev
- DevContainer Specifications: GitHub - devcontainers/spec
- Sample DevContainers: GitHub - devcontainers/templates
- VSCode Documentation: VSCode Dev Containers
- DevContainers Features Docs: GitHub Codespaces
- DevContainers Features: containers.dev/features
- Docker Documentation: Docker Official Docs