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
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
Then from your host machine:
loop-detective --host <docker-host-ip> --port 9229 -d 30
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"
loop-detective --host localhost --port 9229
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
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
Option B: Use socat to forward the port:
docker exec -d my-container socat TCP-LISTEN:9230,fork TCP:127.0.0.1:9229
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
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
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
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
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
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
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.
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:
-
Never bind the inspector to
0.0.0.0in production unless the port is protected by network policies, security groups, or a firewall. -
Prefer SSH tunnels or
kubectl port-forwardover direct remote connections. They provide encryption and authentication. - 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.
- 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
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
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
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)