72.7 Remote Debugging with debugpy (VS Code, PyCharm)
Right, so your code is misbehaving. But it’s misbehaving on a remote server, in a Docker container, or inside a virtual environment so alien it might as well be on the dark side of the moon. You can’t just slap a print("got here lol") statement in there and run it locally. This is where we graduate from caveman debugging to something with a bit more finesse: remote debugging. We’re going to use debugpy, Microsoft’s brilliantly capable debugger protocol for Python. It’s what lets VS Code’s debugger do its magic, and it plays nicely with PyCharm and other modern IDEs too.
The core concept is delightfully simple, even if the setup feels a bit like a spy thriller dead drop. You “inject” a small debugger server (debugpy) into your remote Python process. This server waits, listening on a specific port for a connection. Then, from the comfort of your local machine, your IDE (the client) reaches out across the network to connect to it. Once linked, you’re off to the races: breakpoints, variable inspection, stack traces—the whole glorious works, all as if the code were running on your own machine.
The Basic Incantation: Getting debugpy in the Game
First, you need debugpy installed where your code is running. This is non-negotiable. Toss it into your requirements.txt or install it directly:
pip install debugpy
Now, the most straightforward way to use it is to modify your application’s entry point. You need to import and start the debugger server before your main application code runs. The following snippet is the universal “turn it on” signal.
# your_application.py
import debugpy
# Listen on all interfaces (0.0.0.0) at port 5678
debugpy.listen(("0.0.0.0", 5678))
# Optional: Wait for the client (your IDE) to attach before continuing execution.
# This is a lifesaver if you need to catch early startup code.
print("Waiting for debugger to attach...")
debugpy.wait_for_client()
# ... now proceed with your normal application code ...
import my_main_module
my_main_module.run()
The wait_for_client() call is your decision. Use it if you need to debug something that happens immediately on startup. Your script will pause there, a patient sentinel, until your IDE connects. If you’re debugging a later event (like a web request), you can omit it and set your breakpoints after the server is listening.
Connecting the Dots: Configuring Your IDE
Your remote process is now waiting. Time to connect from home base.
In VS Code, you create a launch configuration in your .vscode/launch.json file. The key is the “connect” setup, not “launch”.
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Attach to Remote",
"type": "python",
"request": "attach",
"connect": {
"host": "your.remote.server.ip",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "/path/to/your/code/on/remote/server"
}
]
}
]
}
The pathMappings section is critically important. It’s how the debugger translates the file paths on your local machine (where you set the breakpoint in ./app/models.py) to the file paths on the remote server (where the code is actually running, e.g., /opt/app/models.py). Get this wrong, and your breakpoints will never fire, leaving you in a special kind of debugging purgatory.
In PyCharm, the process is similar. You create a new “Python Debug Server” run configuration. You input the host, port, and crucially, set up the very same path mappings in the configuration UI. It’s the same concept with a different face.
The “Attach to Process” Lifesaver
What if you can’t easily modify the application’s entry point? Maybe it’s a pre-built Docker image or a complex process manager. Don’t panic. You can often attach debugpy to a running Python process. First, find the PID of your target process.
ps aux | grep python
Then, inject the debugger into it. This is black magic of the highest order, and it’s incredibly useful.
# Run this on the remote machine, targeting the running process
python -m debugpy --listen 0.0.0.0:5678 --pid 1234
This tells debugpy to connect to the existing process with PID 1234 and start a debug server on port 5678. You can then attach from your IDE as normal.
Common Pitfalls and Battle-Tested Advice
Firewalls: This is the number one reason connections fail. Is port 5678 (or whatever you choose) actually open on the remote machine and accessible from your IP? Check your cloud security groups, your local firewall (
ufw), and Docker run commands (-p 5678:5678). I’ve lost hours to this. Don’t be me.Path Mappings, Again: I’m saying it twice because it’s that important. Mismatched paths mean silent, ignored breakpoints. Double and triple-check your
localRootandremoteRoot.Version Mismatch: Try to keep the version of
debugpyon the remote server roughly in sync with the version your local IDE is using. A major version mismatch can lead to weird protocol errors.The Production Caveat: Let’s be absolutely clear. Running a debugger that accepts remote connections on a production server is a gargantuan security risk. You are literally opening an execution port. This is a tool for staging, development, and test environments. If you must debug production, do it on a isolated instance, tunnel the connection through SSH, and shut it down the second you’re done. Seriously.