Skip to content
VS Code

VS Code

Container debugging through VS Code, using Node.js as the concrete example. A Dockerfile contains build instructions, an image is the packaged filesystem and startup configuration produced by those instructions, and a container is one running instance of that image.

Debug a Node.js container with VS Code

Regular Container Tools flow

The regular flow requires the VS Code Container Tools extension, listed as Docker in Extensions. The extracted application directory is opened as the workspace. A workspace is the host directory VS Code treats as the root of the project:

code <APPLICATION_DIRECTORY>

The image is built and started from the existing Dockerfile:

Dockerfile -> right click -> Build Image...
-> enter an image tag or keep the default

Build Image from the Dockerfile context menu in VS Code

Containers panel -> Images -> <IMAGE> -> <TAG> -> Run

Run or Run Interactive from the image tag in the VS Code Containers panel

Run Interactive performs the same start while keeping the container logs visible. Once the application is running, the run icon under Run and Debug uses the selected project launch configuration to attach the debugger. A launch configuration is a .vscode/launch.json entry that tells VS Code which debugger to use, whether to launch or attach, and how local source paths correspond to paths in the running process.

Only one container can bind a particular host port at a time. Bind for 0.0.0.0:<PORT> failed: port is already allocated means that an earlier container or another local process already owns that port. The earlier container can be stopped from Containers panel -> Containers -> <CONTAINER> -> right click -> Stop before starting the debug container.

Container files remain accessible from:

Containers panel -> Containers -> <CONTAINER> -> Files

Browse files inside a running container from the VS Code Containers panel

An interactive shell is available from:

Containers panel -> Containers -> <CONTAINER> -> right click -> Attach Shell

Dockerfile debugger error

VS Code may report that no extension is available for debugging the active Dockerfile and offer to search the Marketplace. The project launch configuration supplies the application debug target.

VS Code reporting that no extension is available for debugging a Dockerfile

It appears when the active editor contains a Dockerfile and no applicable project launch configuration is selected. Run and Debug then treats that file as the requested debug target. A Dockerfile contains the instructions for building an image. The application process running inside the resulting container is the required attachment target.

The project launch configuration under Run and Debug is the regular attachment method. When no launch configuration is available, Dev Containers provides access to the already-running container:

Dev Containers extension
-> Ctrl+Shift+P
-> Dev Containers: Attach to Running Container...
-> <CONTAINER>
-> File -> Open Folder -> <REMOTE_ROOT>

Dev Containers starts a separate VS Code window whose file operations and terminals run inside the selected container. The bottom-left Container: ... indicator confirms that the new window uses this remote environment. Paths opened in that window are container paths rather than paths on the host.

A Dockerfile COPY instruction stores the source files as an image layer at build time. The running container therefore contains a snapshot rather than a live reference to the host directory. Later host changes require another Build Image... unless a bind mount explicitly maps the host directory over the container path.

Find by: vscode, container tools, docker debug, dockerfile debugger error, run and debug, dev containers, attach running container, remote root, copy snapshot

Attach to a running Node.js process inside the container

Opening a Dev Containers window provides access to the container filesystem and process list. It does not attach the JavaScript debugger automatically. Attaching connects VS Code’s debugger to an already-running Node.js process instead of starting another copy of the application.

Dev Containers window -> <REMOTE_ROOT>
-> Ctrl+Shift+P
-> Debug: Attach to Node Process
-> node <ENTRYPOINT>
-> breakpoints under <REMOTE_ROOT>

When the image starts through CMD ["npm", "start"], the process list commonly contains both an npm parent and the actual node <ENTRYPOINT> application process. The Node.js process is the debug target.

npm start
└── node <ENTRYPOINT>    <- attach here

Find by: vscode, dev containers, attach node process, running container, npm parent, node entrypoint, remote debugging, breakpoint

Prepare the image for VS Code debugging

The complete rebuild-based workflow changes package.json and the Dockerfile, then adds .vscode/launch.json for VS Code. Node Inspector is the debugging interface built into Node.js; it permits a debugger to pause execution, inspect variables, and control stepping.

Add the debug command to package.json

Current configuration:

{
  "scripts": {
    "start": "node <ENTRYPOINT>"
  }
}

Updated configuration with a separate Node Inspector command:

{
  "scripts": {
    "start": "node <ENTRYPOINT>",
    "debug": "node --inspect=0.0.0.0:9229 <ENTRYPOINT>"
  }
}

Start the debug command from the Dockerfile

Current configuration containing the CMD line to replace:

EXPOSE <CONTAINER_APP_PORT>
CMD ["npm", "start"]

Updated configuration with EXPOSE 9229 added and the existing CMD ["npm", "start"] line replaced by CMD ["npm", "run", "debug"]:

EXPOSE <CONTAINER_APP_PORT>
EXPOSE 9229
CMD ["npm", "run", "debug"]

Only the new CMD line remains in the Dockerfile. EXPOSE 9229 records the intended inspector port as image metadata; it does not publish that port on the host. npm run debug starts the application with --inspect=0.0.0.0:9229, which makes Node Inspector listen on every container network interface whenever the rebuilt image runs.

Add the VS Code attachment configuration

The documented Dockerfile uses /app as its working directory and keeps src/ as a subdirectory:

WORKDIR /app
COPY ./src ./src

For this source layout, .vscode/launch.json is added exactly as shown. No values require replacement:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to containerized Node.js",
      "type": "node",
      "request": "attach",
      "address": "127.0.0.1",
      "port": 9229,
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/app",
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

localRoot identifies the source tree opened on the host. ${workspaceFolder} is replaced by the current VS Code workspace directory. remoteRoot identifies the same source tree inside the container. VS Code joins the remainder of each file path to these two roots, producing this mapping:

${workspaceFolder}/src/index.js -> /app/src/index.js

The modified image is rebuilt and started through the regular Container Tools flow. Docker: Attach to Node under Run and Debug then connects VS Code to the inspector at 127.0.0.1:9229.

A hollow breakpoint marked as unbound means that the debugger has not matched the local file and line to a script loaded by the Node.js process. For the documented layout, ${workspaceFolder}/src/index.js must refer to the same file as /app/src/index.js. A different WORKDIR, COPY destination, or workspace root requires corresponding localRoot and remoteRoot values.

Find by: rebuild debug image, package json debug script, dockerfile inspect, node inspector, inspect 9229, launch json, expose port

Place breakpoints and inspect variable scope

A breakpoint on a function declaration may pause only while the module initially defines the function. A breakpoint intended to pause on every call belongs on an executable statement inside the function body.

const transformedValue = transform(inputValue);
const nextValue = consume(transformedValue);

A breakpoint on the first line exposes inputValue before transform() runs. transformedValue does not exist yet. Stepping over the line executes transform() and makes its return value available. A breakpoint on the second line exposes both values immediately.

F10         Step Over    execute the current line without entering the called function
F11         Step Into    enter the function called by the current line
Shift+F11   Step Out     finish the current function and return to its caller
F5          Continue     resume execution until the next breakpoint

A scope is the region of code in which a variable name can be resolved. Function parameters and variables declared inside the current function appear under Local. A closure is the retained outer scope used by an inner function; module-level or outer-function variables referenced by the current function appear under Closure. Runtime-provided names available throughout the process appear under Global.

The call stack records the active chain of function calls. Each listed stack frame represents one paused function call and has its own local and closure scopes. Selecting another frame changes which variables Hover, Watch, and the Debug Console resolve.

Find by: breakpoint placement, step over, step into, step out, continue, f10, f11, shift f11, local scope, closure, watch, debug console

Watch variables across scopes

A Watch expression is a variable name or JavaScript expression that VS Code evaluates every time execution pauses. It can be added through the + icon on the right side of the WATCH heading. VS Code resolves it from the selected stack frame, regardless of whether the referenced name appears under Local, Closure, or Global.

WATCH -> + -> <VARIABLE_OR_EXPRESSION>

A username variable added to the VS Code Watch panel

Watch expressions remain visible while stepping through the application. The selected stack frame controls the scope used to resolve each expression.

Find by: vscode watch, watch variable, watch expression, search variables, local, closure, global, paused stack frame, inspect value

Send requests to the debug instance

A debugger attached to a local container observes only requests handled by that container.

request -> http://127.0.0.1:<HOST_APP_PORT> -> local container -> breakpoint
request -> remote deployment                         -> no local breakpoint

The browser or interception proxy destination must point to the published local application port. Changing only the HTTP Host header does not necessarily change the network destination selected by the proxy.

Authentication state belongs to the application instance that created it. A session cookie from a remote deployment may fail against the local process when session storage, signing secrets, or application state differ.

Find by: local container, remote target, breakpoint not hit, proxy destination, host header, published port, session cookie, debug instance