Comprehensive guide to remote debugging techniques for production and development environments. Covers SSH port forwarding, VSCode Remote Development, JetBrains Gateway, container debugging (Docker), Kubernetes debugging, production-safe debugging, debug symbols, and source maps.
Last Updated: 2025-10-26
Remote debugging enables debugging applications running on remote servers, containers, or cloud environments. This guide covers safe and effective remote debugging strategies.
Local Machine (Debugger)
↓ SSH / Network
Remote Machine (Debuggee)
↓
Debug Server (gdbserver, debugpy, etc.)
↓
Application Process
✅ Development on remote server (cloud VM)
✅ Docker container debugging
✅ Kubernetes pod debugging
✅ Production issue investigation (with caution)
✅ IoT / embedded device debugging
✅ Cross-platform debugging (Windows ↔ Linux)
❌ Local development (use local debugger)
❌ Production without monitoring (risky)
❌ High-throughput systems (performance impact)
Forward remote port to local machine:
# Forward remote debugger port 5678 to local 5678
ssh -L 5678:localhost:5678 user@remote-server.com
# Now connect local debugger to localhost:5678
# It tunnels to remote-server.com:5678
Example: Python debugpy:
# On remote server:
python -m debugpy --listen 0.0.0.0:5678 --wait-for-client script.py
# On local machine:
ssh -L 5678:localhost:5678 user@remote-server.com
# In VSCode: Attach to localhost:5678
Forward local port to remote machine:
# Make local port 8080 accessible on remote at port 9090
ssh -R 9090:localhost:8080 user@remote-server.com
# Remote can now access localhost:9090 → your local 8080
Create SOCKS proxy for multiple ports:
# Dynamic proxy on local port 1080
ssh -D 1080 user@remote-server.com
# Configure browser/app to use SOCKS proxy localhost:1080
# All connections tunnel through SSH
Keep tunnel alive:
# Auto-reconnect on disconnect
ssh -L 5678:localhost:5678 -o ServerAliveInterval=60 -o ServerAliveCountMax=3 user@remote-server.com
# Or use autossh
autossh -M 0 -L 5678:localhost:5678 user@remote-server.com
Install extension:
Extensions → Search "Remote - SSH" → Install
Connect to remote:
1. Cmd+Shift+P → "Remote-SSH: Connect to Host"
2. Enter: user@remote-server.com
3. Select platform: Linux / Windows / macOS
4. Opens new VSCode window connected to remote
Now:
- File Explorer shows remote files
- Terminal is remote shell
- Debugger runs on remote
- Extensions installed on remote
SSH Config (~/.ssh/config):
Host my-remote
HostName 192.168.1.100
User myuser
Port 22
IdentityFile ~/.ssh/id_rsa
ForwardAgent yes
# Connect in VSCode: "Remote-SSH: Connect to Host" → my-remote
Install Python extension on remote:
VSCode → Remote connection → Extensions → Install Python
launch.json (remote debugging):
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File (Remote)",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": false
}
]
}
Workflow:
1. Open remote folder in VSCode
2. Set breakpoint in Python file
3. F5 to debug
4. Debugger runs on remote, UI in local VSCode
Debug inside Docker container:
1. Install "Remote - Containers" extension
2. Cmd+Shift+P → "Remote-Containers: Attach to Running Container"
3. Select container
4. Debug inside container environment
devcontainer.json:
{
"name": "Python Dev",
"image": "python:3.11",
"extensions": [
"ms-python.python"
],
"forwardPorts": [5678],
"postCreateCommand": "pip install debugpy"
}
Setup:
1. Download JetBrains Gateway
2. New Connection → SSH
3. Enter: user@remote-server.com
4. Select IDE (PyCharm, IntelliJ, etc.)
5. Gateway installs IDE backend on remote
6. Opens IDE UI locally, runs on remote
Advantages:
✅ Full IDE features on remote
✅ Large codebase stays on remote (fast indexing)
✅ Automatic port forwarding
✅ Integrated debugger
Remote debugging:
1. Open project on remote via Gateway
2. Set breakpoints
3. Run → Debug (Shift+F9)
4. Debugger runs on remote, UI local
VSCode attach:
# Start container with debugger port exposed
docker run -p 5678:5678 my-image
# In VSCode launch.json:
{
"name": "Python: Attach to Docker",
"type": "python",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}
]
}
Python container example:
# Dockerfile
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
RUN pip install debugpy
COPY . .
# Start with debugger
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "--wait-for-client", "app.py"]
# Build and run
docker build -t my-app .
docker run -p 5678:5678 my-app
# Attach VSCode debugger to localhost:5678
Attach to running container:
# Find container ID
docker ps
# Exec into container
docker exec -it <container-id> /bin/bash
# Install debugger
pip install ipdb
# Add breakpoint in code
import ipdb; ipdb.set_trace()
# Run app, drops into debugger in terminal
docker-compose.yml:
services:
app:
build: .
ports:
- "5678:5678"
environment:
- DEBUGPY_ENABLED=true
volumes:
- .:/app # Mount source for live editing
command: python -m debugpy --listen 0.0.0.0:5678 --wait-for-client app.py
# Start with compose
docker-compose up
# Attach debugger to localhost:5678
Debug running pod (Kubernetes 1.23+):
# Create ephemeral debug container in pod
kubectl debug my-pod -it --image=busybox --target=my-container
# Or with debug utilities
kubectl debug my-pod -it --image=nicolaka/netshoot --target=my-container
# Inside debug container:
# Can inspect processes, network, filesystem
ps aux
netstat -tuln
ls /proc/1/root/app
Debug with copy:
# Create copy of pod for debugging
kubectl debug my-pod --copy-to=my-pod-debug --container=my-container
# Modify container in copy
kubectl debug my-pod --copy-to=my-pod-debug --container=my-container --image=python:3.11-debug
Forward pod port to local:
# Forward pod port 5678 to local 5678
kubectl port-forward pod/my-pod 5678:5678
# Attach debugger to localhost:5678
Forward service:
# Forward service port
kubectl port-forward service/my-service 5678:5678
Deploy with debugger:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: my-app:latest
ports:
- containerPort: 5678 # Debug port
env:
- name: DEBUG_MODE
value: "true"
command: ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "--wait-for-client", "app.py"]
# Apply deployment
kubectl apply -f deployment.yaml
# Forward port
kubectl port-forward deployment/my-app 5678:5678
# Attach debugger to localhost:5678
✅ Use ephemeral containers for live debugging
✅ Create pod copy for intrusive debugging
✅ Use port-forward for debugger access
✅ Remove debug configuration from production
✅ Use separate debug environment/namespace
❌ Don't debug production pods directly (use copy)
❌ Don't leave debug ports exposed
❌ Don't modify production deployments for debugging
Safety rules:
1. NEVER pause execution in production
→ Use logging, tracing, metrics instead
2. MINIMIZE performance impact
→ Use sampling, not continuous debugging
3. ISOLATE debugging sessions
→ Debug copy of pod/container, not live instance
4. TIME-BOX sessions
→ Set max debug duration, auto-timeout
5. AUDIT access
→ Log who debugged what and when
6. REVERT changes
→ Never leave debug code in production
Observe without modifying:
# ❌ Bad: Modify state in production
def debug_user(user_id):
user = get_user(user_id)
user.role = 'admin' # DANGEROUS IN PRODUCTION
save_user(user)
# ✅ Good: Observe only
def debug_user(user_id):
user = get_user(user_id)
logger.info(f"User state: {user}") # Read-only
# Don't modify, just observe
Use structured logging instead of breakpoints:
import logging
import json
logger = logging.getLogger(__name__)
def process_order(order):
# Instead of breakpoint(), log context
logger.info("Processing order", extra={
"order_id": order.id,
"user_id": order.user_id,
"items": [item.id for item in order.items],
"total": order.total
})
result = calculate_total(order)
logger.info("Order processed", extra={
"order_id": order.id,
"result": result
})
return result
Debug subset of requests:
import random
def should_debug():
# Debug 1% of requests
return random.random() < 0.01
def handle_request(request):
if should_debug():
logger.debug("Full request context", extra={
"headers": request.headers,
"body": request.body,
"params": request.params
})
# Normal processing
return process(request)
Auto-disable debug mode:
import time
DEBUG_ENABLED_UNTIL = None # Unix timestamp
def enable_debug(duration_seconds):
global DEBUG_ENABLED_UNTIL
DEBUG_ENABLED_UNTIL = time.time() + duration_seconds
logger.warning(f"Debug mode enabled for {duration_seconds}s")
def is_debug_enabled():
if DEBUG_ENABLED_UNTIL is None:
return False
if time.time() > DEBUG_ENABLED_UNTIL:
logger.info("Debug mode expired")
return False
return True
def process_data(data):
if is_debug_enabled():
logger.debug(f"Processing: {data}")
# Normal processing
C/C++ with GDB:
# Compile with debug symbols
gcc -g program.c -o program
# Strip symbols for production (smaller binary)
strip program -o program-stripped
# Debug with separate symbol file
gcc -g program.c -o program
objcopy --only-keep-debug program program.debug
strip program
objcopy --add-gnu-debuglink=program.debug program
# GDB loads symbols automatically
gdb program
(gdb) file program
(gdb) symbol-file program.debug
Rust:
# Debug build (includes symbols)
cargo build
# Release build (optimized, no symbols)
cargo build --release
# Release with debug symbols
cargo build --release --config profile.release.debug=true
Go:
# Build with symbols (default)
go build -o myapp
# Strip symbols
go build -ldflags="-s -w" -o myapp
# Delve debugger
dlv exec ./myapp
Microsoft Symbol Server (Windows debugging):
Use symbol server for Windows binaries:
SRV*c:\symbols*https://msdl.microsoft.com/download/symbols
Linux debug symbols:
# Ubuntu: install debug symbols
sudo apt install <package>-dbg
# Example: Python debug symbols
sudo apt install python3.11-dbg
TypeScript:
// tsconfig.json
{
"compilerOptions": {
"sourceMap": true,
"inlineSourceMap": false, // Separate .map files
"inlineSources": false,
"sourceRoot": "/"
}
}
Webpack:
// webpack.config.js
module.exports = {
devtool: 'source-map', // Production: separate files
// devtool: 'inline-source-map', // Development: inline
// devtool: 'eval-source-map', // Fastest rebuild
};
Vite:
// vite.config.js
export default {
build: {
sourcemap: true, // Generate .map files
}
}
Browser DevTools:
Settings (⚙️) → Sources → Enable JavaScript source maps
Loads .map files automatically:
app.min.js
app.min.js.map // Loaded automatically
Debug shows original TypeScript/source code
Node.js:
# Enable source maps
node --enable-source-maps app.js
# Stack traces show original source, not transpiled
Security considerations:
❌ Don't expose source maps publicly
→ Reveals source code to attackers
✅ Host source maps separately, restrict access
✅ Use Sentry/error tracking to symbolicate server-side
✅ Strip source maps from public builds
Sentry source maps:
# Upload source maps to Sentry
sentry-cli releases files <release> upload-sourcemaps ./dist --rewrite
# Sentry symbolicates errors using uploaded maps
# Users never see source maps
Remote script:
import debugpy
# Listen on all interfaces, port 5678
debugpy.listen(("0.0.0.0", 5678))
print("Waiting for debugger...")
debugpy.wait_for_client() # Block until debugger attaches
# Your code
def main():
for i in range(10):
print(f"Iteration {i}")
if __name__ == "__main__":
main()
VSCode attach:
{
"name": "Python: Remote Attach",
"type": "python",
"request": "attach",
"connect": {
"host": "remote-server.com",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}
]
}
Start with inspector:
# Remote Node.js
node --inspect=0.0.0.0:9229 app.js
# Or debug-brk (pause at start)
node --inspect-brk=0.0.0.0:9229 app.js
Chrome DevTools:
1. SSH tunnel: ssh -L 9229:localhost:9229 user@remote
2. Chrome: chrome://inspect
3. Configure: Add localhost:9229
4. Click "inspect" under Remote Target
VSCode attach:
{
"name": "Node: Attach to Remote",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}
Remote debugging:
# On remote server
dlv debug --headless --listen=:2345 --api-version=2 --accept-multiclient
# Local machine
dlv connect remote-server.com:2345
VSCode attach:
{
"name": "Go: Connect to Remote",
"type": "go",
"request": "attach",
"mode": "remote",
"remotePath": "/app",
"port": 2345,
"host": "remote-server.com"
}
Start JVM with debug agent:
# Remote Java
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar
IntelliJ remote debug:
1. Run → Edit Configurations
2. Add → Remote JVM Debug
3. Host: remote-server.com
4. Port: 5005
5. Click Debug
❌ NEVER: Debug production without copy/staging
→ Pausing production = downtime
❌ NEVER: Leave debug ports exposed to internet
→ Security vulnerability, remote code execution
❌ NEVER: Commit debug code (breakpoint(), debugger;)
→ Pauses production app
❌ NEVER: Debug without SSH tunnel on public network
→ Unencrypted debug traffic
❌ NEVER: Modify production state while debugging
→ Data corruption, undefined behavior
❌ NEVER: Debug high-traffic endpoints
→ Performance impact affects all users
✅ ALWAYS: Use SSH tunnels for remote debugging
✅ ALWAYS: Debug on copy/staging, not production
✅ ALWAYS: Time-limit debug sessions
✅ ALWAYS: Use read-only debugging in production
✅ ALWAYS: Prefer logging/tracing over breakpoints
✅ ALWAYS: Remove debug configuration after session
✅ ALWAYS: Audit who debugged what and when
Remote debugging enables debugging applications in remote environments:
Quick start (Python):
# Remote server
python -m debugpy --listen 0.0.0.0:5678 --wait-for-client app.py
# Local machine
ssh -L 5678:localhost:5678 user@remote-server.com
# VSCode: Attach to localhost:5678
Production debugging:
# Use logging, not breakpoints
logger.info("Debug context", extra={"user_id": user.id, "state": state})
# Sample 1% of requests
if random.random() < 0.01:
logger.debug("Full request", extra={"request": request})
Master remote debugging for efficient distributed system troubleshooting.