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:

  1. Understand containerization and how it differs from virtual machines
  2. Master Dev Container configuration with devcontainer.json
  3. Configure port forwarding for local development
  4. Understand bind mounts and volumes for file persistence
  5. 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

  1. devcontainer.json: Complete configuration file
  2. Dockerfile (optional): Custom image with additional tools
  3. Post-creation script: Automates project setup
  4. 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 install runs 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.

  1. Install Docker Desktop:
    • Download from docker.com
    • Ensure Docker is running (whale icon in menu bar)
  2. Install VS Code Remote Extensions:
    • Install “Dev Containers” extension (ms-vscode-remote.remote-containers)
  3. Verify Docker works:
docker run hello-world
# Should print "Hello from Docker!"
  1. 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.

  1. Create .devcontainer folder:
mkdir .devcontainer
  1. 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"
}
  1. Open in container:
    • Press Cmd+Shift+P → “Dev Containers: Reopen in Container”
    • Wait for container to build (first time takes 1-2 minutes)
  2. 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.

  1. 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
  1. 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"
}
  1. Rebuild container:
    • Cmd+Shift+P → “Dev Containers: Rebuild Container”
  2. Verify custom tools:
aws --version
ts-node --version
nodemon --version

Phase 4: Lifecycle Scripts (30 minutes)

Goal: Automate project setup.

  1. 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!"
  1. Make it executable:
chmod +x .devcontainer/post-create.sh
  1. Update devcontainer.json:
{
  "postCreateCommand": "bash .devcontainer/post-create.sh",
  "postStartCommand": "echo '🟢 Container started'",
  "postAttachCommand": "echo '🔗 Attached to container'"
}
  1. 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.

  1. Container environment variables:
{
  "containerEnv": {
    "NODE_ENV": "development",
    "DEBUG": "app:*"
  }
}
  1. 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.

  1. 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.

  1. 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:
  1. 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"
}
  1. 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

  1. Have a teammate clone the repo
  2. They should only need: Docker Desktop, VS Code, Dev Containers extension
  3. “Reopen in Container” should fully set up the environment
  4. 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

  1. “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.”

  2. “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.”

  3. “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 forwardPorts array. 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


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