Project 5: The "Works on My Machine" Killer (Dev Containers)
Project 5: The “Works on My Machine” Killer (Dev Containers)
Project Overview
| Attribute | Value |
|---|---|
| Difficulty | Advanced |
| Time Estimate | 1 Week |
| Main Language | Dockerfile / JSON |
| Alternative Languages | Bash |
| Knowledge Area | Virtualization / Containers |
| Prerequisites | Docker Desktop installed |
Learning Objectives
By completing this project, you will:
- Understand containerization and how it differs from virtual machines
- Master Dev Container configuration with devcontainer.json
- Configure port forwarding for local development
- Understand bind mounts and volumes for file persistence
- Create lifecycle hooks for automated setup
The Core Question
“How do I onboard a new developer in 5 minutes instead of 5 hours?”
Deep Theoretical Foundation
The “Works on My Machine” Problem
Every developer has experienced this:
Developer A: "The app runs fine on my machine."
Developer B: "I'm getting a module not found error."
Developer A: "Did you install the dependencies?"
Developer B: "Yes, but I have a different Node version."
Developer A: "Which version? Also, do you have the right Python?"
...3 hours later...
Developer B: "Finally working. I had to downgrade npm."
The root cause: Environment drift. Developers’ machines diverge over time—different OS versions, package managers, library versions, environment variables.
Containerization vs Virtualization
Virtual Machines simulate entire computers:
- Guest OS runs on hypervisor
- Each VM has its own kernel
- Heavy (2GB+ per VM)
- Slow to start (minutes)
Containers share the host kernel:
- Only application code and dependencies
- Lightweight (50-200MB)
- Start in seconds
- Isolated but efficient
Virtual Machine: Container:
┌─────────────────┐ ┌─────────────────┐
│ Application │ │ Application │
├─────────────────┤ ├─────────────────┤
│ Guest OS │ │ Libraries │
├─────────────────┤ └────────┬────────┘
│ Hypervisor │ │
├─────────────────┤ ┌────────┴────────┐
│ Host OS │ │ Container │
└─────────────────┘ │ Runtime │
├─────────────────┤
│ Host OS │
└─────────────────┘
VS Code Remote Development Architecture
Dev Containers use a split architecture:
┌─────────────────────────────────────────────────────────┐
│ Your Laptop (VS Code UI) │
│ ┌───────────────────────────────────────┐ │
│ │ Renderer Process (just the UI) │ │
│ └──────────────────┬────────────────────┘ │
│ │ Network (HTTP over Docker socket) │
│ ┌──────────────────┴────────────────────┐ │
│ │ Docker Container (VS Code Server) │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Extension Host (runs in container)│ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Language Servers (in container) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ Your Code (bind-mounted) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Key Insight: The VS Code UI runs on your local machine, but ALL the intelligence—language servers, linters, formatters, debuggers—runs inside the container.
Bind Mounts: Your Code Lives on Host
Your project folder is bind-mounted into the container:
Host Machine: Container:
/Users/you/my-project → /workspaces/my-project
When you save a file in VS Code, it writes to the host filesystem through the mount. If the container is destroyed, your code is safe on the host.
Port Forwarding
When a server runs inside the container (e.g., port 3000), you can’t access it from your host browser by default. Port forwarding creates a tunnel:
Browser: Container:
localhost:3000 → 172.17.0.2:3000
VS Code automatically detects common ports and offers to forward them.
Project Specification
What You’re Building
A fully reproducible development environment that:
- Contains specific versions of Node.js, Python, and essential tools
- Pre-installs required VS Code extensions
- Configures environment-specific settings
- Runs post-creation setup scripts
- Forwards development server ports
Deliverables
- devcontainer.json: Complete configuration file
- Dockerfile (optional): Custom image with additional tools
- Post-creation script: Automates project setup
- Documentation: Team onboarding instructions
Success Criteria
- Running “Reopen in Container” builds successfully
- All team members get identical environments
- Node/Python versions match production
- Extensions are automatically installed
- Ports are correctly forwarded
npm installruns automatically on container creation
Solution Architecture
File Structure
my-project/
├── .devcontainer/
│ ├── devcontainer.json # Main configuration
│ ├── Dockerfile # Custom image (optional)
│ └── post-create.sh # Setup script
├── src/
│ └── ...
├── package.json
└── ...
devcontainer.json Schema
{
// Container identity
"name": "My Project",
// Base image OR Dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:18",
// OR
"build": { "dockerfile": "Dockerfile" },
// VS Code customizations
"customizations": {
"vscode": {
"extensions": ["..."],
"settings": { "...": "..." }
}
},
// Network
"forwardPorts": [3000, 5432],
// Lifecycle scripts
"postCreateCommand": "npm install",
"postStartCommand": "npm run dev"
}
Phased Implementation Guide
Phase 1: Prerequisites and Setup (30 minutes)
Goal: Verify Docker is working.
- Install Docker Desktop:
- Download from docker.com
- Ensure Docker is running (whale icon in menu bar)
- Install VS Code Remote Extensions:
- Install “Dev Containers” extension (ms-vscode-remote.remote-containers)
- Verify Docker works:
docker run hello-world
# Should print "Hello from Docker!"
- Create a test project:
mkdir devcontainer-test && cd devcontainer-test
npm init -y
echo "console.log('Hello from container!');" > index.js
Phase 2: Basic Dev Container (45 minutes)
Goal: Create a working container configuration.
- Create .devcontainer folder:
mkdir .devcontainer
- Create devcontainer.json:
{
"name": "Node.js Development",
"image": "mcr.microsoft.com/devcontainers/javascript-node:18",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint"
],
"settings": {
"terminal.integrated.defaultProfile.linux": "bash"
}
}
},
"forwardPorts": [3000],
"postCreateCommand": "npm install"
}
- Open in container:
- Press
Cmd+Shift+P→ “Dev Containers: Reopen in Container” - Wait for container to build (first time takes 1-2 minutes)
- Press
- Verify environment:
# In container terminal
node --version # Should show v18.x
npm --version # Should show 10.x
which node # Should show /usr/local/bin/node
Phase 3: Custom Dockerfile (45 minutes)
Goal: Add tools not in the base image.
- Create Dockerfile:
# .devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/typescript-node:18
# Install additional tools
RUN apt-get update && apt-get install -y \
git \
curl \
jq \
&& rm -rf /var/lib/apt/lists/*
# Install global npm packages
RUN npm install -g typescript ts-node nodemon
# Install AWS CLI (example of complex tool)
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" \
&& unzip awscliv2.zip \
&& ./aws/install \
&& rm -rf aws awscliv2.zip
- Update devcontainer.json to use Dockerfile:
{
"name": "Full Stack Development",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-azuretools.vscode-docker"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
},
"forwardPorts": [3000, 5432],
"postCreateCommand": "npm install"
}
- Rebuild container:
Cmd+Shift+P→ “Dev Containers: Rebuild Container”
- Verify custom tools:
aws --version
ts-node --version
nodemon --version
Phase 4: Lifecycle Scripts (30 minutes)
Goal: Automate project setup.
- Create post-create script:
#!/bin/bash
# .devcontainer/post-create.sh
echo "🚀 Setting up development environment..."
# Install dependencies
npm install
# Create local environment file
if [ ! -f .env ]; then
cp .env.example .env
echo "📝 Created .env from .env.example"
fi
# Setup database (if needed)
if [ -f "scripts/setup-db.sh" ]; then
bash scripts/setup-db.sh
fi
echo "✅ Development environment ready!"
- Make it executable:
chmod +x .devcontainer/post-create.sh
- Update devcontainer.json:
{
"postCreateCommand": "bash .devcontainer/post-create.sh",
"postStartCommand": "echo '🟢 Container started'",
"postAttachCommand": "echo '🔗 Attached to container'"
}
- Understand lifecycle hooks:
| Hook | When It Runs | Use Case |
|---|---|---|
postCreateCommand |
After container is created | npm install, db setup |
postStartCommand |
Every container start | Start dev server |
postAttachCommand |
When VS Code attaches | Welcome message |
Phase 5: Environment Variables and Secrets (30 minutes)
Goal: Handle configuration securely.
- Container environment variables:
{
"containerEnv": {
"NODE_ENV": "development",
"DEBUG": "app:*"
}
}
- Mount secrets from host (for credentials):
{
"mounts": [
"source=${localEnv:HOME}/.aws,target=/home/vscode/.aws,type=bind,readonly"
]
}
This mounts your AWS credentials from your host machine into the container.
- VS Code forwards SSH agent automatically:
- Git SSH authentication works inside the container
- No need to copy SSH keys
Phase 6: Multi-Service with Docker Compose (45 minutes)
Goal: Add database and other services.
- Create docker-compose.yml:
# .devcontainer/docker-compose.yml
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ..:/workspaces/my-project:cached
command: sleep infinity
ports:
- "3000:3000"
depends_on:
- db
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/myapp
db:
image: postgres:15
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: myapp
ports:
- "5432:5432"
volumes:
postgres-data:
- Update devcontainer.json for Compose:
{
"name": "Full Stack with Database",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/my-project",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"ckolkman.vscode-postgres"
]
}
},
"forwardPorts": [3000, 5432],
"postCreateCommand": "npm install"
}
- Test database connection:
# In container
psql -h db -U postgres -d myapp
# Password: postgres
Complete devcontainer.json Example
{
"name": "Ultimate Dev Stack",
"build": {
"dockerfile": "Dockerfile",
"context": ".."
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"prisma.prisma",
"ms-azuretools.vscode-docker",
"ckolkman.vscode-postgres"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"terminal.integrated.defaultProfile.linux": "zsh",
"files.watcherExclude": {
"**/node_modules/**": true
}
}
}
},
"containerEnv": {
"NODE_ENV": "development",
"LOG_LEVEL": "debug"
},
"mounts": [
"source=${localEnv:HOME}/.aws,target=/home/vscode/.aws,type=bind,readonly",
"source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly"
],
"forwardPorts": [3000, 5432, 6379],
"portsAttributes": {
"3000": {
"label": "Web App",
"onAutoForward": "openBrowser"
},
"5432": {
"label": "PostgreSQL"
}
},
"postCreateCommand": "bash .devcontainer/post-create.sh",
"postStartCommand": "npm run dev",
"remoteUser": "vscode"
}
Testing Strategy
Verification Checklist
- Container builds without errors
- All extensions install correctly
- Node/npm versions match expected
- Port forwarding works (localhost:3000)
- Database connection works (if applicable)
- Git operations work (SSH agent forwarding)
- Files persist after container rebuild
Onboarding Test
- Have a teammate clone the repo
- They should only need: Docker Desktop, VS Code, Dev Containers extension
- “Reopen in Container” should fully set up the environment
- They should be able to run the app within 5 minutes
Common Pitfalls and Debugging
Pitfall 1: Container Build Fails
Problem: Dockerfile syntax error or missing package.
Solution: Build manually to see detailed errors:
docker build -f .devcontainer/Dockerfile -t test .
Pitfall 2: Port Already in Use
Problem: “Port 3000 is already in use” error.
Solution:
- Check what’s using the port:
lsof -i :3000 - Or use a different port in devcontainer.json
Pitfall 3: File Permission Issues
Problem: Can’t write files, “Permission denied” errors.
Solution: Ensure correct user in Dockerfile:
USER vscode
And in devcontainer.json:
"remoteUser": "vscode"
Pitfall 4: Slow File System
Problem: npm install is extremely slow on macOS.
Solution: Use named volumes for node_modules:
"mounts": [
"source=my-project-node_modules,target=/workspaces/my-project/node_modules,type=volume"
]
Interview Questions
-
“What are the benefits of developing inside a container?”
Answer: “Containers ensure every developer has identical environments—same OS, same package versions, same tools. This eliminates ‘works on my machine’ issues. New developers can be productive in minutes instead of hours. The environment is version-controlled with the code.”
-
“How do you handle credentials in a Dev Container?”
Answer: “VS Code automatically forwards the SSH agent, so Git authentication works. For other credentials, I bind-mount config files from the host (like ~/.aws) as read-only. Secrets never go in the Dockerfile or devcontainer.json.”
-
“How does port forwarding work in Dev Containers?”
Answer: “When a server runs in the container, VS Code creates a tunnel from localhost on the host to the container’s port. You configure this in the
forwardPortsarray. VS Code also auto-detects common ports and offers to forward them.”
Resources
Essential Reading
| Resource | Topic |
|---|---|
| Docker for Developers (Gomes) | Docker fundamentals |
| How Linux Works (Ward) | Container isolation |
| VS Code Dev Containers Documentation | Official reference |
Microsoft Dev Container Images
- mcr.microsoft.com/devcontainers
- Pre-built images for Node, Python, Go, Rust, etc.
Self-Assessment Checklist
- I can create a basic devcontainer.json
- I can write a custom Dockerfile for additional tools
- I understand bind mounts vs volumes
- I can configure port forwarding
- I can use Docker Compose for multi-service setups
- I can handle credentials securely
- I can debug container build failures
- A teammate can onboard using my Dev Container in under 5 minutes
Previous: P04-dynamic-snippet-library.md Next: P06-focus-mode-workspace-profiles.md