How VS Code Remote Development Works: SSH, Containers, and Tunnels
▶ Watch on YouTube & subscribe to The Stack Underflow
You open a folder on an SSH server. VS Code behaves exactly as it does locally: IntelliSense fires, the debugger steps through code, the terminal runs commands on the remote machine. No lag that breaks the illusion. No architecture you had to configure. It just works — because almost nothing about the underlying process model actually changed.
That non-obvious sameness is the whole story. VS Code’s architecture was always a collection of processes communicating over message channels. Going remote means one of those channels stretches across a network. The processes are identical; only the wire got longer.
The one-sentence version: When VS Code goes remote, the UI stays on your machine and every other process moves to the remote machine — the only architectural difference is that one local IPC channel becomes a network tunnel.
The local baseline: what all those processes are
Before a network appears anywhere, it helps to be precise about what VS Code actually runs on your laptop. Electron is the shell — a Chromium browser instance hosting the editor UI and a main Node.js process managing the application lifecycle. Inside that shell, VS Code spawns at least three distinct processes:
Your Laptop (local, all processes on one machine)
+-----------------------------------------------------+
| Electron Main Process |
| | |
| +-- Renderer Process (the UI you see) |
| | [Monaco editor, panels, tree views] |
| | |
| +-- Extension Host (Node.js, isolated) |
| | |
| +-- Language Servers (per language) |
| | [LSP: completions, diagnostics] |
| | |
| +-- Debug Adapters (per debugger) |
| | [DAP: breakpoints, step, watch] |
| | |
| +-- Integrated Terminal (pty/ConPTY) |
+-----------------------------------------------------+
All channels = local IPC (Unix socket / named pipe)
Latency: microseconds
The Extension Host is the process that matters most for remote development. It is a separate Node.js process — deliberately isolated from the UI renderer — that runs every installed extension. Language servers are child processes the Extension Host spawns and talks to over the Language Server Protocol (LSP), a JSON-RPC protocol Microsoft designed so that one language intelligence implementation can serve any editor. Debug adapters use the Debug Adapter Protocol (DAP), the same idea applied to debuggers. The integrated terminal attaches to a pty (pseudoterminal) — on Linux and macOS a POSIX pty, on Windows a ConPTY layer that emulates terminal semantics over the Win32 Console API.
All of this runs comfortably on the same machine. Now ask: what has to change when the machine that holds your code is somewhere else?
What the VS Code Server actually is
The answer is: not much. When you connect VS Code to a remote target, it partitions itself into two halves.
Your Laptop Remote Machine
+----------------+ network +----------------------------+
| Electron UI | | VS Code Server (Node.js) |
| (thin client) | <--tunnel--> | Extension Host |
| | | Language Servers |
+----------------+ | Debug Adapters |
| Integrated Terminal |
| Your Code (filesystem) |
+----------------------------+
The remote half is called the VS Code Server. It is a small Node.js binary that VS Code’s Remote Development extensions automatically download and install on the remote machine the first time you connect. It lives at ~/.vscode-server/ on the remote host, pinned to the exact commit hash of your local VS Code build — the client and server must match versions.
The VS Code Server hosts the Extension Host, all language servers, all debug adapters, and the shell session for the integrated terminal. The local side retains only the Electron UI renderer. The tunnel between them carries the same IPC message protocol that ran across a few bytes of local socket — it just now crosses a network.
As of VS Code 1.99 (March 2025), prebuilt servers require Linux kernel 4.18 or later, glibc 2.28 or later, libstdc++ 3.4.25 or later, and binutils 2.29 or later (code.visualstudio.com/docs/remote/faq, 2025). Older distributions need a custom sysroot workaround. Alpine Linux is explicitly unsupported because it uses musl libc instead of glibc.
Three remote targets, one underlying model
Remote Development ships as four extensions, each wrapping the same VS Code Server model around a different transport:
| Extension | Transport | What “remote” means | Filesystem access |
|---|---|---|---|
| Remote - SSH | SSH tunnel (AES-256 CTR) | Any SSH-accessible host or VM | Remote host filesystem |
| Dev Containers | Docker exec channel | A container running on local or remote Docker | Container filesystem (local files mounted in) |
| WSL | Local pipe to WSL instance | Windows Subsystem for Linux | WSL filesystem |
| Remote - Tunnels | Microsoft Dev Tunnels relay | Any machine, no firewall/SSH required | Remote host filesystem |
All four install the VS Code Server on the target, connect the Electron UI to it, and let the Extension Host run on the target machine beside your code. The structural diagram above applies to every one of them.
How Remote - SSH bootstraps
When you connect to an SSH host for the first time, VS Code:
Step 1 Open SSH connection (using your local ssh binary or built-in client)
Step 2 Check ~/.vscode-server/ on remote for matching commit hash
Step 3 If absent: download vscode-server tarball to remote via SCP,
or download it on the remote directly over HTTPS to
update.code.visualstudio.com (falls back to SCP if no internet)
Step 4 Extract to ~/.vscode-server/bin/<commit-hash>/
Step 5 Start the server process; it opens a local socket and prints
a JSON handshake token to stdout
Step 6 VS Code reads the token, upgrades the SSH channel to carry
the VS Code IPC protocol
Step 7 Extension Host starts; workspace extensions install themselves
on the remote if not already present
After step 6, port forwarding happens transparently: VS Code forwards ports that workspace extensions or devcontainer.json declare, surfacing them in the Ports panel.
How Remote - Tunnels work without SSH
Remote - Tunnels solves the “no open firewall, no SSH port” problem. Run code tunnel on the remote machine. The CLI starts the VS Code Server, then opens an outbound WebSocket to Microsoft Dev Tunnels — a relay service hosted in Azure. Your local VS Code (or a browser pointing at vscode.dev) connects to the same relay, and the relay splices the two connections together. Neither side needs to accept inbound connections.
Remote Machine Azure Relay Your Browser / VS Code
+--------------+ outbound WS +-----------+ outbound WS +------------------+
| code tunnel | --------------> | Dev Tunnel| <-------------- | vscode.dev or |
| (VS Code | | Service | | VS Code Desktop |
| Server) | <=============> | (AES-256) | <=============> | (UI only) |
+--------------+ bidirectional +-----------+ bidirectional +------------------+
End-to-end encryption is AES-256 CTR. GitHub or Microsoft account authentication gates access. Accounts are limited to 10 active tunnels. The machine-to-vscode.dev URL takes the form https://vscode.dev/tunnel/<machine-name>/<folder>.
Extension classification: what runs where
Not all extensions follow the code to the remote machine. VS Code uses the extensionKind field in each extension’s package.json to decide where it runs:
| Kind | Declared as | Where it runs | What it can access |
|---|---|---|---|
| UI extension | "extensionKind": ["ui"] | Local Extension Host (always) | Local UI, themes, keybindings — not remote filesystem |
| Workspace extension | "extensionKind": ["workspace"] | Remote Extension Host (inside VS Code Server) | Remote filesystem, remote shell, language servers |
| Flexible extension | ["ui", "workspace"] | VS Code picks the best host | Both, depending on context |
Themes, color schemes, keymaps, and snippet packs are UI extensions — they live on your laptop regardless of where the code lives. Linters, language servers, debuggers, and build tools are workspace extensions — they must run on the remote machine because they need to read your files and execute programs in that environment. This is why VS Code sometimes prompts you to “Install on Remote” even for an extension already installed locally: the remote Extension Host has its own extension folder and needs its own copy.
If an extension uses non-VS Code APIs — raw Node.js fs, child_process, shell scripts — VS Code cannot automatically relocate it. Extension authors must test against remote scenarios and declare the correct extensionKind. The API guide at code.visualstudio.com/api/advanced-topics/remote-extensions covers the full compatibility checklist.
vscode.dev and the web extension host
vscode.dev is the browser-only variant: the entire Electron UI runs as static JavaScript files served from a CDN, and the Extension Host runs as a Web Worker inside the browser sandbox — no Node.js at all. This is called the web extension host.
Web extensions have the same VS Code API surface but no access to Node.js built-ins (fs, child_process, net). Purely declarative extensions (grammars, snippets, themes) work unchanged. Language servers and debuggers — which require spawning child processes — either need a bundled web-compatible implementation or simply do not run in the browser-only mode.
Connect vscode.dev to a code tunnel and the architecture reverts to the full model: the web UI sends messages to the relay, the relay forwards them to the VS Code Server on the remote machine, and the full Node.js Extension Host runs on the remote with access to everything. vscode.dev becomes the thin-client UI; the heavy lifting stays on your remote machine.
Dev Containers: the VS Code Server in a Docker container
With Dev Containers, the remote target is a Docker container rather than a network host. VS Code mounts your local workspace directory into the container (-v $(pwd):/workspaces/project), then installs and starts the VS Code Server inside the container process. The Electron UI talks to it over a Docker exec channel rather than an SSH connection, but the IPC protocol is identical.
Your Host OS
+---------------------------------------------------+
| VS Code (Electron UI only) |
| |
| Docker Engine |
| +-----------------------------------------------+|
| | Container (e.g. node:22-bullseye) ||
| | ~/.vscode-server/ (installed by VS Code) ||
| | Extension Host ||
| | Language Servers ||
| | Your Code (mounted from host) ||
| +-----------------------------------------------+|
+---------------------------------------------------+
The container definition lives in .devcontainer/devcontainer.json. A minimal example that pins an image, forwards a port, and pre-installs an extension:
{
"image": "mcr.microsoft.com/devcontainers/typescript-node:22",
"forwardPorts": [3000],
"customizations": {
"vscode": {
"extensions": ["dbaeumer.vscode-eslint"]
}
}
}
Dev Container Features — modular install scripts from containers.dev — let teams compose standard toolchains (Docker-in-Docker, GitHub CLI, AWS CLI) without baking them into a custom image. The spec is tooling-agnostic and supported by GitHub Codespaces, JetBrains Gateway, and the Dev Container CLI.
How AI editors plug into this skeleton
Cursor, Windsurf, and GitHub Copilot all use this same process architecture. Understanding how they differ is a direct consequence of understanding it.
GitHub Copilot is a VS Code workspace extension. It runs inside the Extension Host — which means it runs on the remote machine in a remote session. It calls the Copilot API over HTTPS from wherever the Extension Host lives. The extension API gives it access to open documents and editor state that VS Code exposes; it cannot reach deeper editor internals.
Cursor is a hard fork of code-oss — the MIT-licensed open-source base that Microsoft uses to build VS Code. The fork preserves the Electron shell, the Extension Host process boundary, and the full LSP/DAP infrastructure. It then adds AI features that require access below the extension API: deep hooks into the renderer process for inline completions, direct access to TextModel objects (VS Code’s in-memory file representation), and a parallel hidden Electron window that was the “shadow workspace” (used until January 2025 when it was retired in favor of agentic tool use due to its 500MB to 2GB RAM cost per instance). These capabilities are only possible because Cursor controls the editor binary, not just an extension inside it.
Windsurf follows the same fork strategy — code-oss base, Cascade AI replacing the extension-layer intelligence — for the same architectural reason: direct access to file system events, terminal output, and editor state that the extension sandbox does not expose.
The key distinction:
GitHub Copilot = VS Code + extension in the Extension Host
Cursor / Windsurf = code-oss fork with AI wired BELOW the extension API
In June 2025, Microsoft open-sourced the GitHub Copilot Chat extension (MIT license, github.com/microsoft/vscode-copilot-chat) and announced plans to refactor its AI components into VS Code core. The inline completions extension (previously closed source) is next in line. This gradually closes the gap between what a fork can do versus what a first-party extension can do — but the extension sandbox boundary remains.
How to apply this right now
For remote work:
- Use Remote - SSH for stable, low-latency connections to a known host (corporate VM, cloud VM with SSH access). The SSH tunnel carries all VS Code IPC directly; there is no relay in the middle.
- Use Remote - Tunnels (
code tunnelon the remote machine) when you cannot open an SSH port or need access from a browser. Up to 10 tunnels per GitHub/Microsoft account. - Use Dev Containers when you want reproducible, team-shared environments. The
.devcontainer/devcontainer.jsonfile becomes your “works on my machine” solution. - Use GitHub Codespaces when you want the Dev Containers model but with zero local Docker dependency and cloud-hosted compute.
For extension authors:
- Set
extensionKindexplicitly. If your extension spawns processes, reads~/.config, or calls local CLIs, it is a workspace extension and must run on the remote. Test withRemote-SSHbefore releasing. - Avoid hardcoded local paths. Use
vscode.env.remoteNameto detect the remote context andvscode.workspace.fsinstead of rawfsso VS Code routes file I/O correctly.
For understanding the glibc wall:
- VS Code 1.99+ (March 2025) requires glibc 2.28 on the remote host. Debian 10+, Ubuntu 20.04+, RHEL 8+ all qualify. CentOS 7 and Debian 9 do not without a workaround sysroot. Plan ahead when provisioning remote hosts.
Common misconceptions
“Going remote adds a completely new architecture.” It does not. The VS Code Server is literally the non-UI half of VS Code, repackaged to run without a display. The message protocol it uses with the Electron UI is the same local IPC that runs on a laptop — just routed through a network socket.
“The VS Code Server is a cloud service I have to manage.” In the SSH and tunnel cases, VS Code downloads and manages the server binary automatically, pinned to your current VS Code version. It lives at ~/.vscode-server/ on the remote. You do not provision it separately, and it has no persistent daemon — it starts when you connect and stops when you disconnect.
“Extensions work the same locally and remotely.” UI extensions (themes, keymaps) do. Workspace extensions (language servers, linters, debuggers) must be installed on the remote Extension Host. A local install does not propagate. VS Code will prompt you, but it is worth understanding why: the extension needs to run next to the code and toolchain, not next to your screen.
“Cursor and Windsurf are just VS Code with a plugin.” They are forks of code-oss with AI wired below the extension API boundary. The distinction matters: a plugin cannot spawn a hidden editor window, directly manipulate TextModel objects, or hook into the rendering pipeline. A fork can. The fork cost is tracking upstream manually — new VS Code features arrive on a delay, and niche extensions can break at fork boundaries.
Frequently asked questions
What exactly is the VS Code Server and where does it live on my machine?
It is a Node.js binary that VS Code installs at ~/.vscode-server/bin/<commit-hash>/ on the remote host. Each VS Code release gets its own subdirectory keyed to the commit hash, so multiple VS Code versions can coexist on the same remote host without conflict. The server starts on demand when you connect and stops when you disconnect.
Can I run the VS Code Server without VS Code Desktop — in a browser only?
Yes. code tunnel on the remote machine starts the VS Code Server and prints a vscode.dev/tunnel/... URL. Open that URL in any browser, authenticate with GitHub or Microsoft, and you get the full VS Code UI running in the browser, connected to your remote machine. No VS Code Desktop required.
Why does VS Code ask me to install an extension “on the remote”?
Workspace extensions run inside the VS Code Server on the remote machine. Installing an extension locally only registers it with the local Extension Host. The remote Extension Host has its own extension folder and needs its own copy to run language analysis, linting, or debugging against your code on the remote filesystem.
Does remote development need a constant internet connection?
It needs a connection to the remote machine. Remote - SSH over a local network works fine with no internet. Remote - Tunnels requires internet connectivity to the Azure relay service for the duration of the session. The inline development experience (typing, completions, terminal) works as long as the tunnel is alive; reconnection is automatic on dropout.
How does VS Code handle the case where the remote host does not have internet access?
For SSH remotes without internet, VS Code can transfer the server binary from your local machine via SCP (the fallback path in the bootstrap sequence). Set remote.SSH.localServerDownload: "always" to force this path. For completely air-gapped hosts, you can manually copy the server tarball and set remote.SSH.serverInstallPath to point at it.
How does this relate to GitHub Codespaces?
Codespaces is Dev Containers running in a Microsoft-hosted cloud VM, with the VS Code Server pre-installed and the Dev Tunnel relay built into the Codespace URL. When you open a Codespace in VS Code Desktop, it connects exactly like a Remote - Tunnel. When you open it in the browser, vscode.dev is the UI. Same architecture, Microsoft’s infrastructure instead of yours.
Where this fits in the series
This is episode 5 and the finale of the first playlist in The Engine Behind Every AI Code Editor. The series started by decomposing VS Code into its separate processes, traced how those processes communicate, and has now shown that the same model stretches cleanly across a network. Two episodes to read alongside this one:
- Why Cursor, Windsurf, and Copilot Are All Built on VS Code — the fork vs extension distinction in full depth
- How Autocomplete and Go-to-Definition Work — the LSP layer that the remote Extension Host runs
- Why VS Code Survives Extension Crashes — the process isolation that makes the thin-client split safe
- What Happens When an Agent Runs a Command — how AI agents interact with the same terminal and shell infrastructure described here
The next playlist moves up the stack into AI agent internals — how tools like Claude Code and Cursor hook into this architecture to run real work. Browse all tutorials to follow along.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →