How to Setup PyCharm's Remote Debugger for Docker

For the past year or so I have been diving into the somewhat crazy world of containers, particularly Docker on a daily basis. The benefits tend to outweigh the negatives (atleast in a development environment) however it does have it's quirks in terms of setting up a local environment. You have to think of things such as:

  1. Auto-reloading
  2. Mapping local paths to remote [container] paths
  3. Setting up simple development environments using tools such as (or only) Docker Compose

Etc.—All of which turned out to be achievable fairly quickly except for one thing; a debugger. As a result, this article shall be illustrating in fairly reasonably detail how to setup a remote debugger using PyCharm's debug server and their pydevd library.

Do bare in mind that you generally do not need to install pydevd seperately using pip. The only reason we are doing so here is because the application itself resides on a different host (the container itself) whereas PyCharm is installed on our development machine.

Note: I will not be diving into the debugger itself, what it is and how to use it


Components

The structure is a rather simple Client <-> Server architecture where you have:

  1. A debug server, which is your host machine hosted by PyCharm
  2. A debug client, which is your Python container with pydevd installed

The two will then have to communicate together, which brings us to our next part.

Setup Server

Starting off with the debug server, we first need to configure a Debug Configuration in PyCharm. As of PyCharm 29/12/2017, this can be done by navigating to Run -> Edit Configuration -> + -> Python Remote Debug and having the configuration set to something simple such as:

You can click OK and proceed to run your debugger server which will display at the bottom like so:

One last important thing to do, is keep note of the host IP that can be accessed by your container/docker host. This can be found by running ifconfig or ipconfig and searching for a valid IP. In my case, it looked something along the lines of:

$ ipconfig
>>> Ethernet adapter VirtualBox Host-Only Network #2:
>>>
>>>   Connection-specific DNS Suffix  . :
>>>   Link-local IPv6 Address . . . . . : fe80::483d:468a:aa5d:79b8%7
>>>   IPv4 Address. . . . . . . . . . . : 192.168.33.1
>>>   Subnet Mask . . . . . . . . . . . : 255.255.255.0
>>>   Default Gateway . . . . . . . . . :

Therefore I now know that my host machine's IP is 192.168.33.1.

Setup Client

Now that the trickier part is complete, we move over to our client which is our Python container. All we need to do, is use the pydevd library to communicate with our remote debug server to act as a breakpoint. So anywhere in your code (that you know will execute) add something along the lines of (remember to change the IP):

import pydevd  
pydevd.settrace('192.168.33.1', port=4444, stdoutToServer=True, stderrToServer=True)  

And once the code hits, we finally see the breakpoint inside our PyCharm IDE:


Extras

To avoid having to keep copying and pasting the above code, I sometimes decide to write wrapper/utility classes that I can use globally irrespective of the host and port. They would look something along the lines of:

class DebuggerClient:  
    """Shell class that stores properties to be used by the debugger

    Exposes methods to be used for debugging purposes
    """

    def __init__(self, server_host, server_port):
        self.host = server_host
        self.port = int(server_port)

    def breakpoint(self):
        """Sets a breakpoint for the debug server to catch

        Will only set the breakpoint should the debugger client is set;
         this is done to ensure that in a non-development environment
         any possible lingering breakpoints don't accidentally hang
         any threads.
        """
        import pydevd
        from config import ENVIRONMENT

        if ENVIRONMENT.lower() == 'development':
            pydevd.settrace(self.host, port=self.port, stdoutToServer=True, stderrToServer=True)


class Debugger:  
    """Assigns the debugger client to be used as a class variable

    Only will allow a single debugger client to be assigned at any
     given time.
    """

    debugger = None  # :class:DebuggerClient

    def __init__(self):
        pass

    @classmethod
    def set_debugger_client(cls, debugger):
        """Sets the debugger client"""
        cls.debugger = debugger

You would then need to only instantiate the Debugger client once at the start of your application and then use it whenever.

Debugger.set_debugger_client(DebuggerClient(REMOTE_DEBUG_SERVER_HOST, REMOTE_DEBUG_SERVER_PORT))  

Additionally there is a flag that only executes the breakpoint if we're in a DEVELOPMENT environment.

Juxhin Dyrmishi Brigjaj

Read more posts by this author.