Have you ever tried hunting for a bug in a Django app that’s running in a Docker container? It’s not always a trivial process and can have you dotting print statements all over the place because you can’t figure out how to use breakpoints properly inside the Docker container.

Well, fortunately there is a solution to this. 2 solutions even.

Solution 1: Configure VS Code to debug a dockerised app

Step 1 - Configure VS Code

VS Code has built in debugging support for a variety of languages. For local files and scripts this is fairly simple - you can press F5 and VS Code will try and run the currently active file.

Of course, Django is a little more complex than this, and for this scenario VS Code provides the ability to define and save a debug configuration. This is done through the use of a JSON file called launch.json. You can create one yourself (it is simply a JSON file, after all), or select the debug icon (the ‘play’ symbol with the little bug on it on the left toolbar) and you will be given the option to create a launch.json file.

Run and debug menu option

Most likely VS Code will then identify your project as a Django app, or at the very least, a Python one, but you can largely ignore this and copy/paste the following configuration.

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Django - Debug",
            "type": "python",
            "request": "attach",
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}/src",
                    "remoteRoot": "/<app folder in container>"
                }
            ],
            "port": 3000,
            "host": "127.0.0.1",
        }
    ]
}

Some of this should be fairly self-explanatory.

  • name is unimportant and just needs to be something meaningful to you.
  • type as we can see is python.
  • request can be one of 2 values - launch or attach. In our case we are going to attach to a running container.
  • localRoot is the path of your application in the local environment. ${workspaceFolder} is the VS Code value for the root of your project. src is a common location to put the actual application code (things like Dockerfile, requirements.txt etc. being in the root itself). Your project may have a differntly named folder here (app is common, as is the project name itself), so change this accordingly.
  • remoteRoot is the name of the application folder in the container. This will be defined in your Dockerfile, but follow most tutorials or instructions and you will likely end up with it called app. Make sure the value is correct for the settings in your own Dockerfile.
  • port is the port VS Code will use to connect to the container for debugging. The port number is unimportant, as long as it does not clash with the port your application is using (which in most cases means don’t use port 8000 for Django apps).
  • host will simply be your localhost IP.

Step 2 - Configure your project

Now that VS Code is configured, we need to tell your Django project that it’s going to be looking for debug information on a specific port. The easiest way to do this is to use the debugpy package. This will need installing into your Docker container when building for development, but should not be used when deploying to production (I will write a post another time on using multiple Dockerfiles and multi-stage builds).

Once you have ensured debugpy will be installed, make the following changes to your manage.py file for the project.

def main():
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")

	  if settings.DEBUG:
        if os.environ.get("RUN_MAIN") or os.environ.get("WERKZEUG_RUN_MAIN"):
            import debugpy
  
            debugpy.listen(("0.0.0.0", 3000))
            print("Press F5 in VS Code to attach debugger...", flush=True)
            debugpy.wait_for_client()
            print("VS Code debugger attached...", flush=True)

This will check the DEBUG flag (which should only ever be set in a development environment, and never in production!), and if it is set, import debugpy to allow VS Code to connect to the project.

  • debugpy.listen(("0.0.0.0", 3000)) listens on port 3000 for communication from VS code (if you chose a different port number in your launch.json you should use that here).
  • The Press F5 print statement will just tell you when to go to your project in VS Code and hit F5 to connect it to the container (which can only be done once debugpy is listening).
  • debugpy.wait_for_client() then waits for information from VS Code on port 3000.

Step 3 - Debug your project

Start your project up using Docker as usual in a shell. You will see something like this:

Recreating myproject-db-1 ... done
Recreating myproject_myproject_1 ... done
Attaching to myproject_db_1, myproject_myproject_1
db_1            |
db_1            | PostgreSQL Database directory appears to contain a database; Skipping initialization
db_1            |
db_1            | 2022-09-17 16:26:02.975 UTC [1] LOG:  starting PostgreSQL 13.8 (Debian 13.8-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
db_1            | 2022-09-17 16:26:02.977 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
db_1            | 2022-09-17 16:26:02.977 UTC [1] LOG:  listening on IPv6 address "::", port 5432
db_1            | 2022-09-17 16:26:02.998 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
db_1            | 2022-09-17 16:26:03.020 UTC [26] LOG:  database system was shut down at 2022-09-17 15:22:33 UTC
db_1            | 2022-09-17 16:26:03.039 UTC [1] LOG:  database system is ready to accept connections
myproject_1     | Press F5 in VS Code to attach debugger...

Go to VS Code and hit F5. You should then see similar logs in both the Debug console in VS Code, and the docker logs.

VS Code debugger attached...
2022-09-17 16:29:13 [7] [INFO] pathname=/usr/local/lib/python3.10/site-packages/django/utils/autoreload.py lineno=637 funcname=run_with_reloader Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

September 17, 2022 - 16:29:14
Django version 3.2.15, using settings 'config.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

You can now use the VS Code debugger as you would normally, complete with breakpoints that will drop you into a pdb shell in the debug console and let you interrogate the state at ant given point in your code.


Solution 2: Running the server in the container manually

This solution uses simple docker-compose commands to let you start your container manually and run the server, rather than starting it with docker compose up along with all the other services.

Step 1 - Run all the backing services

Your project likely depends on other services other than your actual application: almost certainly a database, possibly redis, elasticsearch etc. Start these services first so they are running correctly. You can do this as usual using

docker compose up [-d] <service1> <service2> ... <serviceN>

At this point do NOT start your main application.

Step 2 - Start your application

This needs to be started using run, not up. Use the following command:

docker compose run --service-ports -it <container name> bash

This will start your application container, but in a bash shell.

Note:

If this doesn’t work properly, and your container starts, it is likely becuase you are using ENTRYPOINT. It is advisable to use RUN instead as this can be overridden by you when you want to shell into the container.

Step 3 - Start your local server

You should now be in your application container in a shell. Now you can simply run the server as usual. So for Django, this would be:

python manage.py runserver [<IP>:<port>]

Note:

Do not run the server using gunicorn, uwsgi or any other server as these do not attach STDIN and so will not work with breakpoint, pdb etc.


And that’s it - 2 ways to help you debug your application while it’s running in a docker container.

Happy debugging!