Ides Editors

Remote Development with VS Code

A practical guide to remote development with VS Code using SSH, containers, WSL, and GitHub Codespaces for full-featured development on remote machines.

Remote Development with VS Code

Local development has limits. Your laptop might not have enough RAM for the full stack. Your production environment runs Linux but you are on Windows. The codebase lives on a powerful build server. Remote development solves all of these by running VS Code's backend on a remote machine while you interact with the frontend locally.

I switched most of my development to remote setups two years ago. The experience is nearly indistinguishable from local development once configured properly. This guide covers every remote development approach VS Code supports.

Prerequisites

  • VS Code installed (v1.80+)
  • SSH access to a remote machine (for SSH development)
  • Docker installed (for Dev Containers)
  • WSL installed (for WSL development, Windows only)
  • Basic terminal and SSH knowledge

The Remote Development Architecture

VS Code's remote development works by splitting the editor into two parts:

  1. UI Frontend — runs locally, handles rendering, keyboard input, and UI
  2. VS Code Server — runs on the remote machine, handles file access, language features, terminal, and extensions
┌─────────────┐         ┌──────────────────┐
│  Local PC   │         │  Remote Machine  │
│             │  SSH/   │                  │
│  VS Code UI │◄──────►│  VS Code Server  │
│  (frontend) │  WSL/   │  Extensions      │
│             │  Docker │  Terminal         │
│             │         │  File System      │
└─────────────┘         └──────────────────┘

Extensions run on the remote side, so language servers, linters, and debuggers have direct access to the remote file system and tools.

Remote SSH Development

The most common setup. Connect to any machine you can SSH into.

Installation

Install the Remote - SSH extension:

ext install ms-vscode-remote.remote-ssh

Configuring SSH Hosts

VS Code reads your SSH config file (~/.ssh/config). Set up your hosts there:

# ~/.ssh/config

Host dev-server
    HostName 192.168.1.100
    User shane
    Port 22
    IdentityFile ~/.ssh/id_ed25519
    ForwardAgent yes

Host cloud-vm
    HostName dev.example.com
    User ubuntu
    IdentityFile ~/.ssh/cloud_key
    Port 2222
    ServerAliveInterval 60
    ServerAliveCountMax 3

Host jump-server
    HostName 10.0.0.50
    User deploy
    ProxyJump bastion.example.com

Host gpu-box
    HostName ml-server.internal
    User researcher
    ProxyJump jump-server
    LocalForward 8888 localhost:8888
    LocalForward 6006 localhost:6006

Connecting

  1. Press Ctrl+Shift+P → "Remote-SSH: Connect to Host"
  2. Select your host from the list
  3. VS Code opens a new window connected to the remote machine

Or click the green remote indicator in the bottom-left corner.

First Connection Setup

On first connect, VS Code installs its server component on the remote machine. This requires:

  • A compatible OS (Linux x64, ARM64, or macOS)
  • wget or curl available
  • 200MB of disk space in `/.vscode-server/`
# If auto-install fails, manually install the server:
# VS Code tells you the commit hash it needs
curl -L "https://update.code.visualstudio.com/commit:<commit-hash>/server-linux-x64/stable" \
  -o vscode-server.tar.gz
mkdir -p ~/.vscode-server/bin/<commit-hash>
tar -xzf vscode-server.tar.gz -C ~/.vscode-server/bin/<commit-hash> --strip-components 1

Port Forwarding

Access remote services locally. VS Code auto-detects ports and offers to forward them:

// .vscode/settings.json on the remote
{
  "remote.autoForwardPorts": true,
  "remote.autoForwardPortsSource": "process"
}

Manual port forwarding via the command palette:

  1. Ctrl+Shift+P → "Forward a Port"
  2. Enter the port number (e.g., 3000)
  3. Access http://localhost:3000 in your local browser

Or in the SSH config:

Host dev-server
    LocalForward 3000 localhost:3000
    LocalForward 5432 localhost:5432
    LocalForward 6379 localhost:6379

SSH Configuration for Reliability

Host *
    # Keep connections alive
    ServerAliveInterval 30
    ServerAliveCountMax 5

    # Reuse connections (faster reconnection)
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600

    # Compression (helps on slow connections)
    Compression yes

Create the sockets directory:

mkdir -p ~/.ssh/sockets

Workspace Settings for Remote

// .vscode/settings.json
{
  // Reduce file watching on large codebases
  "files.watcherExclude": {
    "**/node_modules/**": true,
    "**/.git/objects/**": true,
    "**/dist/**": true
  },

  // Reduce bandwidth
  "search.followSymlinks": false,

  // Use remote terminal
  "terminal.integrated.defaultProfile.linux": "bash"
}

Dev Containers

Dev Containers package your entire development environment — OS, tools, runtimes, extensions — into a Docker container. Every developer gets the exact same setup.

Installation

ext install ms-vscode-remote.remote-containers

Basic Dev Container Configuration

Create .devcontainer/devcontainer.json in your project:

{
  "name": "Node.js Dev Environment",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",

  "features": {
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },

  "forwardPorts": [3000, 5432],

  "postCreateCommand": "npm install",

  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "humao.rest-client"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode"
      }
    }
  },

  "mounts": [
    "source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,type=bind,readonly",
    "source=${localEnv:HOME}/.gitconfig,target=/home/node/.gitconfig,type=bind,readonly"
  ],

  "remoteUser": "node"
}

Custom Dockerfile Dev Container

For more control, use a custom Dockerfile:

# .devcontainer/Dockerfile
FROM node:20-bullseye

# Install system dependencies
RUN apt-get update && apt-get install -y \
    postgresql-client \
    redis-tools \
    jq \
    httpie \
    && rm -rf /var/lib/apt/lists/*

# Install global npm packages
RUN npm install -g nodemon jest eslint prettier

# Create non-root user workspace
RUN mkdir -p /workspace && chown node:node /workspace

USER node
WORKDIR /workspace
// .devcontainer/devcontainer.json
{
  "name": "Custom Node.js Environment",
  "build": {
    "dockerfile": "Dockerfile",
    "context": ".."
  },
  "forwardPorts": [3000, 5432, 6379],
  "postCreateCommand": "npm install",
  "remoteUser": "node",
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode"
      ]
    }
  }
}

Docker Compose Dev Containers

For multi-service applications, use Docker Compose:

# .devcontainer/docker-compose.yml
version: "3.8"

services:
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - ..:/workspace:cached
      - node_modules:/workspace/node_modules
    ports:
      - "3000:3000"
    depends_on:
      - db
      - redis
    environment:
      DATABASE_URL: postgres://dev:dev@db:5432/myapp
      REDIS_URL: redis://redis:6379

  db:
    image: postgres:15
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  node_modules:
  pgdata:
// .devcontainer/devcontainer.json
{
  "name": "Full Stack Dev",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
  "forwardPorts": [3000, 5432, 6379],
  "postCreateCommand": "npm install",
  "remoteUser": "node"
}

WSL Development

For Windows developers, WSL provides a native Linux environment.

Installation

ext install ms-vscode-remote.remote-wsl

Opening a Project in WSL

# From WSL terminal
cd /home/shane/projects/my-app
code .

# Or from Windows
code --remote wsl+Ubuntu /home/shane/projects/my-app

WSL Performance Tips

// .vscode/settings.json
{
  // Keep files in the Linux filesystem, not /mnt/c/
  // Linux FS is much faster than the Windows mount

  // Reduce file watchers
  "files.watcherExclude": {
    "**/node_modules/**": true,
    "**/.git/objects/**": true
  },

  // Use Linux shell
  "terminal.integrated.defaultProfile.linux": "bash"
}

Critical performance rule: Store your code in the Linux filesystem (/home/user/projects/), not in the Windows mount (/mnt/c/Users/). The Windows mount adds significant I/O overhead.

WSL-Specific Git Configuration

# In WSL
git config --global core.autocrlf input
git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"

GitHub Codespaces

Codespaces provide cloud-hosted dev environments with VS Code in the browser or connected to your local VS Code.

Configuration

Codespaces use the same devcontainer.json format:

// .devcontainer/devcontainer.json
{
  "name": "Codespace",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",

  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {},
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },

  "forwardPorts": [3000],
  "postCreateCommand": "npm install",

  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "GitHub.copilot"
      ]
    },
    "codespaces": {
      "openFiles": ["README.md"]
    }
  }
}

Connecting Local VS Code to Codespaces

# Install the Codespaces extension
# Then: Ctrl+Shift+P -> "Codespaces: Connect to Codespace"

# Or from the command line
gh codespace code --codespace <name>

Codespace Secrets

Set environment variables that are injected into every Codespace:

gh secret set API_KEY --body "your-key-here" --repos owner/repo
gh secret set DATABASE_URL --body "postgres://..." --repos owner/repo

Extension Management for Remote

Some extensions run on the UI side (themes, keybindings), while most run on the remote side (language servers, linters). Control this in your extension:

// In extension's package.json
{
  "extensionKind": ["workspace"]  // Runs on remote
  // or
  "extensionKind": ["ui"]         // Runs locally
  // or
  "extensionKind": ["ui", "workspace"]  // Can run either side
}

Configure where specific extensions run:

// User settings
{
  "remote.extensionKind": {
    "esbenp.prettier-vscode": ["workspace"],
    "ms-vscode.theme-monokai-pro": ["ui"]
  }
}

Complete Working Example: Full Remote Dev Setup

Project structure with all remote configurations:

my-project/
  .devcontainer/
    devcontainer.json
    docker-compose.yml
    Dockerfile
  .vscode/
    settings.json
    launch.json
    tasks.json
    extensions.json
  src/
  package.json
// .devcontainer/devcontainer.json - Works for Codespaces AND local Dev Containers
{
  "name": "My Project Dev",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",

  "features": {
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },

  "forwardPorts": [3000, 5432],
  "portsAttributes": {
    "3000": { "label": "App", "onAutoForward": "openBrowser" },
    "5432": { "label": "PostgreSQL", "onAutoForward": "silent" }
  },

  "postCreateCommand": "npm install && npm run db:migrate",
  "postStartCommand": "npm run dev",

  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "humao.rest-client",
        "cweijan.vscode-database-client2"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "terminal.integrated.defaultProfile.linux": "bash",
        "files.watcherExclude": {
          "**/node_modules/**": true,
          "**/.git/objects/**": true
        }
      }
    }
  },

  "remoteUser": "node",
  "remoteEnv": {
    "NODE_ENV": "development"
  }
}
// .vscode/settings.json - Shared between local and remote
{
  "editor.tabSize": 2,
  "editor.formatOnSave": true,
  "files.trimTrailingWhitespace": true,
  "files.insertFinalNewline": true,
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true
  }
}

Common Issues and Troubleshooting

SSH connection times out

The remote machine might have a firewall blocking the SSH port, or the VS Code server cannot download:

Fix: Test basic SSH first with ssh user@host. If that works but VS Code fails, check if the remote machine has internet access (needed to download the server). Use Remote-SSH: Settings to set remote.SSH.connectTimeout higher.

Dev Container builds but extensions fail to install

The container might not have internet access during the post-create phase:

Fix: Check Docker network settings. Use "features" instead of installing tools in the Dockerfile when possible — features handle caching better.

Files are slow to open on SSH

High latency connections make every file open feel sluggish:

Fix: Enable Compression yes in your SSH config. Use files.watcherExclude aggressively. Consider using a Dev Container on the remote machine instead of plain SSH.

WSL performance is terrible

Files stored on the Windows filesystem (/mnt/c/) go through a translation layer:

Fix: Move your project to the Linux filesystem: cp -r /mnt/c/projects/myapp ~/projects/myapp. The performance difference is dramatic — npm install can be 10x faster.

Port forwarding conflicts

A local service is already using the port VS Code wants to forward:

Fix: Change the local port in the Ports panel (click the port number and edit it). Or stop the conflicting local service.

Best Practices

  • Store devcontainer.json in your repo. This is how you guarantee every developer has the same environment. It works for Codespaces too.
  • Use SSH connection multiplexing. ControlMaster and ControlPersist in your SSH config make reconnections instant.
  • Keep code on the remote filesystem. For SSH and WSL, do not work on mounted Windows filesystems. The I/O overhead is severe.
  • Forward only the ports you need. Auto-forwarding is convenient but can expose services unintentionally. Use portsAttributes to control behavior.
  • Pin your Dev Container base image versions. node:20 today is different from node:20 next month. Use specific tags like node:20.11-bullseye.
  • Use postCreateCommand for one-time setup. Use postStartCommand for things that should run every time the container starts.
  • Test your devcontainer.json in Codespaces. If it works in Codespaces, it works everywhere.

References

Powered by Contentful