DEV Community

Bill Tu
Bill Tu

Posted on

Debugging Node.js in Docker and Kubernetes Without Restarting

There's a Node.js service running in a Kubernetes pod. It's slow. You need to figure out why. But you can't SSH into the container. You can't redeploy with --inspect. You can't attach a debugger the way you would on your laptop.

This is the reality of debugging in containerized environments. The tools that work perfectly in development — Chrome DevTools, node --inspect, clinic.js — all assume you have direct access to the machine running your process. In production Kubernetes, you don't.

With node-loop-detective v1.3.0, you can now connect to a Node.js inspector on any reachable host. One new flag: --host. That's it.

loop-detective --host 10.0.0.42 --port 9229
Enter fullscreen mode Exit fullscreen mode

This article walks through the practical setups for Docker, Kubernetes, and remote servers — and the security considerations you should know about.

The Problem With Remote Debugging

Node.js has a powerful built-in debugger based on the V8 Inspector Protocol. When you start a process with --inspect, it opens a WebSocket endpoint that speaks Chrome DevTools Protocol (CDP). Through this protocol, you can do almost anything: evaluate expressions, capture CPU profiles, take heap snapshots, set breakpoints.

The problem is access. By default, the inspector binds to 127.0.0.1:9229. It's only accessible from the same machine. This is a security feature — the CDP protocol has no authentication. Anyone who can reach the port can execute arbitrary code in your process.

In a container, "the same machine" means inside the container. Your laptop is outside. The inspector is unreachable.

There are three ways to bridge this gap.

Approach 1: Docker with Exposed Inspector Port

The simplest case. Start your Node.js app with the inspector bound to 0.0.0.0 (all interfaces) and map the port:

docker run -p 9229:9229 my-app node --inspect=0.0.0.0:9229 app.js
Enter fullscreen mode Exit fullscreen mode

Then from your host machine:

loop-detective --host <docker-host-ip> --port 9229 -d 30
Enter fullscreen mode Exit fullscreen mode

If Docker is running locally, the host IP is usually 127.0.0.1 or host.docker.internal. For remote Docker hosts, use the machine's IP.

Docker Compose

services:
  api:
    build: .
    command: node --inspect=0.0.0.0:9229 app.js
    ports:
      - "9229:9229"
      - "3000:3000"
Enter fullscreen mode Exit fullscreen mode
loop-detective --host localhost --port 9229
Enter fullscreen mode Exit fullscreen mode

What if the container is already running without --inspect?

If the container runs Linux and you have docker exec access, you can activate the inspector at runtime:

# Find the Node.js PID inside the container
docker exec my-container pgrep -f "node"
# Output: 1

# Send SIGUSR1 to activate the inspector
docker exec my-container kill -SIGUSR1 1
Enter fullscreen mode Exit fullscreen mode

But the inspector binds to 127.0.0.1 inside the container, so you can't reach it from outside. Two options:

Option A: Run loop-detective inside the container:

docker exec -it my-container npx loop-detective 1
Enter fullscreen mode Exit fullscreen mode

Option B: Use socat to forward the port:

docker exec -d my-container socat TCP-LISTEN:9230,fork TCP:127.0.0.1:9229
Enter fullscreen mode Exit fullscreen mode

Then connect from outside via the mapped port.

Approach 2: Kubernetes

Kubernetes adds another layer of network isolation. Pods have their own IP addresses, and they're typically not directly reachable from your workstation.

Using kubectl port-forward

The easiest method. No changes to your deployment needed (assuming the inspector is already open):

# Forward local port 9229 to the pod's port 9229
kubectl port-forward pod/my-app-pod-abc123 9229:9229

# In another terminal
loop-detective --port 9229 -d 30
Enter fullscreen mode Exit fullscreen mode

Since port-forward makes the remote port appear as localhost, you don't even need --host.

Using the pod IP directly

If your network allows direct pod access (e.g., you're on the same VPC, or using a VPN):

# Get the pod IP
kubectl get pod my-app-pod -o jsonpath='{.status.podIP}'
# Output: 10.244.1.15

loop-detective --host 10.244.1.15 --port 9229
Enter fullscreen mode Exit fullscreen mode

Activating the inspector on a running pod

If the pod wasn't started with --inspect, you can activate it via SIGUSR1:

# Get the Node.js PID (usually 1 in a container)
kubectl exec my-app-pod -- pgrep -f "node"

# Activate the inspector
kubectl exec my-app-pod -- kill -SIGUSR1 1

# Forward and connect
kubectl port-forward my-app-pod 9229:9229
loop-detective --port 9229
Enter fullscreen mode Exit fullscreen mode

Helm chart snippet

If you want to make the inspector available in staging/debug environments:

containers:
  - name: api
    command: ["node"]
    args: ["--inspect=0.0.0.0:9229", "app.js"]
    ports:
      - containerPort: 9229
        name: inspector
        protocol: TCP
Enter fullscreen mode Exit fullscreen mode

Don't do this in production without network policies restricting access to the inspector port.

Approach 3: Remote Servers

For traditional VM or bare-metal deployments, SSH tunneling is the safest approach:

# Create an SSH tunnel
ssh -L 9229:127.0.0.1:9229 user@remote-server

# In another terminal
loop-detective --port 9229
Enter fullscreen mode Exit fullscreen mode

The tunnel makes the remote inspector appear as localhost:9229. No need for --host.

If SSH tunneling isn't an option and the inspector is bound to a reachable interface:

loop-detective --host remote-server.example.com --port 9229
Enter fullscreen mode Exit fullscreen mode

loop-detective will print a security warning:

⚠ Warning: Connecting to remote host remote-server.example.com.
  The CDP protocol has no authentication — ensure the network is trusted.
Enter fullscreen mode Exit fullscreen mode

Security Considerations

The Chrome DevTools Protocol is powerful. Through it, you can:

  • Execute arbitrary JavaScript in the target process (Runtime.evaluate)
  • Read environment variables (which often contain secrets)
  • Access the file system through injected code
  • Modify application behavior

There is no authentication, no encryption (unless you tunnel through TLS), and no access control. Anyone who can reach the inspector port owns the process.

Rules of thumb:

  1. Never bind the inspector to 0.0.0.0 in production unless the port is protected by network policies, security groups, or a firewall.
  2. Prefer SSH tunnels or kubectl port-forward over direct remote connections. They provide encryption and authentication.
  3. Close the inspector when you're done. If you activated it via SIGUSR1, the only way to close it is to restart the process. Plan accordingly.
  4. Use network policies in Kubernetes to restrict which pods/namespaces can reach the inspector port.

loop-detective is designed for short diagnostic sessions. Connect, profile, disconnect. Don't leave inspector ports open permanently.

What You Get

Once connected — whether locally, via Docker, or across a Kubernetes cluster — loop-detective gives you the same full diagnostic report:

✔ Connected to Node.js process
  Profiling for 30s with 50ms lag threshold...

⚠ Event loop lag: 245ms at 2025-03-15T14:23:45.123Z
🌐 Slow HTTP: 1820ms GET internal-api:8080/users → 200
🔌 Slow TCP: 950ms postgres-primary:5432

────────────────────────────────────────────────────────────
  Event Loop Detective Report
────────────────────────────────────────────────────────────
  Duration:  30012ms
  Samples:   13521
  Hot funcs: 8

  Diagnosis
────────────────────────────────────────────────────────────
   HIGH  cpu-hog
         Function "processPayload" consumed 54.2% of CPU time
         at /app/src/handlers/payment.js:127
         → Consider breaking this into smaller async chunks

  ⚠ Slow Async I/O Summary
    Total slow ops: 12

    🌐 HTTP — 8 slow ops, avg 1200ms, max 1820ms
      GET internal-api:8080/users
        8 calls, total 9600ms, avg 1200ms, max 1820ms

    🔌 TCP — 4 slow ops, avg 850ms, max 950ms
      postgres-primary:5432
        4 calls, total 3400ms, avg 850ms, max 950ms
Enter fullscreen mode Exit fullscreen mode

CPU profiling, event loop lag detection, slow I/O tracking — all working across the network, all without modifying the target application.

The Full Workflow

Here's the typical production debugging workflow with loop-detective:

# 1. Identify the problematic pod
kubectl get pods -l app=my-service
# my-service-abc123   1/1   Running

# 2. Activate the inspector (if not already open)
kubectl exec my-service-abc123 -- kill -SIGUSR1 1

# 3. Forward the port
kubectl port-forward my-service-abc123 9229:9229 &

# 4. Run the diagnostic
loop-detective --port 9229 -d 60 --io-threshold 500

# 5. For JSON output (pipe to monitoring)
loop-detective --port 9229 -d 30 --json > diagnostic-report.json
Enter fullscreen mode Exit fullscreen mode

Total time from "something is slow" to "here's the function and line number": about 2 minutes. No redeployment. No restart. No code changes.

Try It

npm install -g node-loop-detective@1.3.0
Enter fullscreen mode Exit fullscreen mode

The source is on GitHub: github.com/iwtxokhtd83/node-loop-detective

The --host flag is a small addition — one line in the CLI parser, one config property passed through. But it unlocks the tool for the environments where it's needed most: production containers where you can't just "restart with --inspect."

Top comments (0)