Internals Decoded
Topics/devcontainers
AI Internals

How the VS Code Run Button Works in Dev Containers

Your code doesn't run on your machine. Learn how VS Code tunnels the Debug Adapter Protocol into dev containers to launch and debug processes remotely.

devcontainersvscodedockerdebugginginternals
12 min readUpdated Jun 24, 2026

The green Run button in VS Code is a lie. A useful lie, but a lie. When you press it inside a dev container, you are not launching a process on your machine. You are sending a message to a process that lives inside a container. That process then launches another process inside the same container. Your local VS Code is just a thin client, watching the action through a tunnel.

The button feels local. It is not. And that is the entire point.

A Remote Control for a Race Car

Think of a remote control car. You press the accelerator. The remote sends a radio signal. The car’s onboard computer receives it. The computer tells the motor controller to spin the motor. The motor moves the wheels. You never touch the motor. You only touch the remote.

Now map that to a dev container.

The remote is your local VS Code window. You click the Run button. The signal is a Debug Adapter Protocol message. It travels through a tunnel into the dev container. The car’s onboard computer is the VS Code Server running inside that container. The motor controller is the debug adapter. The motor is your target process. The wheels are your code executing.

Your code never runs on your machine. It runs in the container. The debugger never runs on your machine. It runs in the container. Even the VS Code Server that manages the whole debug session lives inside the container. Your local machine is the remote. The container is the car.

That is the mental model. The rest of this article shows how the pieces fit.

Two Phases, One Button

Clicking Run is not a single event. It is the final step in a long sequence. That sequence breaks into two phases.

Phase 1: you open your project in a dev container. VS Code reads a devcontainer.json file, builds or pulls an image, starts a container, mounts your workspace, and installs a VS Code Server inside the container. This phase sets up the environment.

Phase 2: you press F5 or click the Run button. VS Code resolves a launch configuration, finds the right debug adapter, starts it inside the container, and tunnels the Debug Adapter Protocol between your local UI and the remote adapter. The adapter then launches or attaches to your target process inside the same container.

The button feels instant because Phase 1 is already done. But Phase 2 is far from instant. It is a coordinated dance across process boundaries.

Phase 1: Building the Car

Phase 1 starts the moment you choose “Reopen in Container” or “Open Folder in Container.” The Dev Containers extension reads the .devcontainer/devcontainer.json file from your project. That file is the single source of truth for the environment. It tells VS Code which image to use, how to mount the workspace, which lifecycle hooks to run, and which extensions to install inside the container. devcontainer.json reference

The extension then calls the devcontainer CLI. The CLI is a reference implementation of the Dev Container specification. It can build images, start containers, and run commands inside them. VS Code runs devcontainer up with your workspace folder. The CLI does the heavy lifting. devcontainer CLI

If the devcontainer.json specifies an image, the CLI pulls it. If it specifies a Dockerfile, the CLI builds it. If it uses Docker Compose, the CLI delegates to docker compose and identifies the service that will host the dev container. devcontainer.json options

Once the image is ready, Docker creates a container. The workspace folder is mounted inside it. By default, VS Code bind mounts the host folder into the container. The mount target is set by workspaceFolder. If you used “Clone Repository in Container Volume,” the workspace lives in a Docker volume instead. workspace mounts

Before you ever touch the code, lifecycle hooks may run. onCreateCommand runs once when the container is first created. postCreateCommand runs after the container is assigned to a user. postStartCommand runs every time the container starts. These hooks install dependencies, seed databases, and run setup scripts. When they finish, the environment is ready. lifecycle scripts

Finally, VS Code installs the VS Code Server inside the container. The server is a long running process that hosts the remote extension host, manages the filesystem, and later spawns debug adapters. The local VS Code client connects to this server over a tunneled connection. The status bar shows you are now “in” the dev container. VS Code Remote Development

Phase 1 is complete. The car is built and ready.

Phase 2: Pressing the Accelerator

Now you press F5. The button triggers a debug session. VS Code looks for a launch configuration in .vscode/launch.json. This file defines the debug type, program arguments, environment variables, and other options. If no configuration exists, VS Code may auto detect one based on the project structure. launch configurations

But here is the first twist: the launch configuration is resolved on the remote side. The VS Code Server, running inside the container, reads the workspace file. The server knows the container’s filesystem paths. The client only sees the UI. So the configuration file is read from the mounted workspace inside the container. No local file access is needed.

Next, VS Code locates the debug adapter. Each debug adapter is contributed by an extension that registers a debug type. For example, the Node.js debugger extension registers the node type. The Python extension registers python. In a dev container, these extensions are installed and activated in the remote extension host. They live inside the container. They have access to the container’s toolchain. debug extensions

The VS Code client then sends a request to start a debug session. The server receives it. The server spawns the debug adapter process inside the container. The adapter is a separate process that understands the Debug Adapter Protocol (DAP). It communicates with VS Code via DAP messages. The client and adapter do not talk directly. The messages are tunneled through the server connection. Debug Adapter Protocol

The debug adapter now takes over. It launches or attaches to the target process. For a Node.js app, the adapter spawns a node process with the --inspect flag. For a C++ app, it might invoke gdb with a machine interface. The target process runs inside the same container as the adapter. It shares the container’s filesystem, libraries, and environment variables. debug adapter implementation

From this point forward, the control loop is standard DAP. The client sends setBreakpoints, continue, next. The adapter executes them against the target. It sends back stopped, terminated, output events. The UI updates. All of this happens remotely. The adapter and target are inside the container. The client is just rendering the state.

If your application listens on a port, Docker may expose it to the host. VS Code’s automatic port forwarding can also forward container ports to localhost. You can open a browser and hit http://localhost:3000. The request goes from your host to the container, where your code is running. port forwarding

The button worked. The code ran. The debugger controlled it. But the code and the debugger never touched your machine.

The Stack, Layer by Layer

Let’s walk through each layer that makes this possible.

devcontainer.json

The devcontainer.json file is the contract. It declares the image, the mounts, the hooks, the extensions. It is read by the Dev Containers extension and the devcontainer CLI. It is also read by other tools like GitHub Codespaces. The spec is open. The file is portable. When you click Run, you are relying on the fact that this file has already been instantiated. The container has the right tools because the file said so. devcontainer spec

devcontainer CLI

The CLI is the orchestrator. It translates the devcontainer.json into Docker commands. It runs docker build or docker pull. It starts containers with the right mounts and environment variables. It runs lifecycle hooks. It can also be used standalone. For example, you could run devcontainer exec to run a command inside the container with the same configuration VS Code would use. The CLI makes the environment reproducible. devcontainer CLI GitHub

Docker

Docker is the runtime. It creates the container, sets up the network namespace, mounts volumes, and runs processes. From Docker’s perspective, a dev container is just a container with some extra volumes and environment variables. The VS Code Server is just another process inside it. The debug adapter is another. The target process is another. When you click Run, Docker is the layer that actually spawns the adapter and target processes. Docker

VS Code Server

The server is the brain in the container. It hosts the remote extension host, manages the filesystem, and spawns terminals and debug adapters. It talks to the client over a persistent, multiplexed connection. The protocol is not fully public, but the conceptual model is a pipe carrying JSON messages. When you click Run, the server receives the debug session request and starts the adapter. It then tunnels DAP messages between the client and the adapter. VS Code Server architecture

Debug Adapter

The adapter is a process that translates DAP into language specific operations. It launches the target, sets breakpoints, steps through code, and reports back. It runs inside the container. It shares the same process namespace as the target. It can attach to an already running process or start a new one. The adapter is contributed by an extension. That extension is installed in the remote extension host. So the adapter lives entirely in the container. debug adapter

Target Process

The target is your code. It runs inside the container. It reads the workspace files from the mounted volume. It uses the container’s libraries and environment. It can be a web server, a CLI tool, a test runner. The debug adapter controls it. The client reflects its state.

Why This Matters

The dev container Run button is not a gimmick. It is a design choice that solves real problems.

First, it guarantees consistency. Every developer who clones the repo and opens the dev container gets the same toolchain. The debug adapter uses the same runtime version, the same libraries, the same dependencies. No more “works on my machine” for debugging. The environment is versioned in devcontainer.json.

Second, it enables remote development. The container can run on a remote Docker host. The local machine is just a thin client. You can debug a process running on a cloud VM with the same UI. The debug pipeline is the same. Only the location of the container changes.

Third, it separates concerns. The local machine does not need to have the runtime or the debugger installed. The container has everything. The client is just a viewer. This makes onboarding fast. You can switch projects without polluting your host.

Fourth, it makes debugging multi service setups easier. With Docker Compose, you can have a web service and a database service. The debug adapter runs in the web service container. It can connect to the database container by its service name. The networking is handled by Docker. Your local machine does not need to run a database.

Quick Reference

ConceptDetail
Key file.devcontainer/devcontainer.json
CLI command to startdevcontainer up --workspace-folder .
Server process inside containercode-server (VS Code Server)
Debug protocolDebug Adapter Protocol (DAP)
Default workspace mountbind mount of host folder into container
Lifecycle hooksonCreateCommand, postCreateCommand, postStartCommand, postAttachCommand
Port forwardingautomatic or via devcontainer.json forwardPorts
Debug adapter locationinside container, spawned by VS Code Server

Frequently Asked Questions

Q: Where does the VS Code Server come from? Does it get baked into the image? No. The Dev Containers extension copies the server binaries into the running container after it starts. The server is the same version as your local VS Code. It is not part of the image. This keeps the image generic and the server version consistent with your client.

Q: What if I don’t have a launch.json? Can I still debug? Yes. VS Code tries to auto detect a debug configuration based on the project structure and the active file. For example, if you have a .js file open and Node.js is installed, it may offer to generate a Node.js launch config. The auto detection runs in the remote extension host, so it uses the container’s runtime.

Q: Can I debug a program that needs a GUI? No. The dev container does not have a display server by default. The debug adapter and target process run headless. You could forward X11 or use a VNC server inside the container, but that is outside the scope of the standard dev container tooling.

Q: Why does the debugger sometimes fail to attach if I set breakpoints before launching? The debug adapter must initialize the breakpoints before the target process starts executing the code you care about. If you set a breakpoint in a file that is not yet loaded, the adapter may not be able to resolve it until the process runs. Some adapters handle this gracefully; others require you to set breakpoints in already loaded files. This is a language specific behavior, not a dev container limitation.

Q: Does the target process run as root inside the container? It depends. The container’s default user is often root, but devcontainer.json can specify a remoteUser. The debug adapter and target process run as that user. You can set "remoteUser": "vscode" to run as a non root user. The VS Code Server also runs as the same user. This is configurable. remoteUser

When the Button Becomes Boring

The Run button is the most boring part of a dev container. It does exactly what you expect. But that is only true because the machinery underneath is so carefully orchestrated. The button is a proxy. The real work happens in a container, through a tunnel, across process boundaries. The next time you press F5, think about the remote control car. You are not touching the motor. You are just sending a signal.

If you want this kind of breakdown every week, how real systems actually work under the hood, subscribe to Internals Decoded at internalsdecoded.com.

Sources

Weekly internals
One breakdown every week
How Docker, Git, Kubernetes, VS Code, and the tools you use every day actually work. No fluff. Built for senior engineers.
Subscribe free →
Explore more
/topics
Browse all topics
More deep dives on AI internals — inference, agents, vector databases, MCP, and more.