“If you want a thing done well, do it yourself.” ― Napoleon Bonaparte
This tutorial is valid for these platforms:
- Linux ✅
- Windows ❌
- OSX ❌
Changes will be indicated along with a changelog.
Table Of Contents
- Table Of Contents
- 1. Requirements
- 2. Motivation
-
3. How to set up for this example
- 3.1. Clone the project and have some requirements ready
- 3.2. Enable virtual environment
- 3.3. Add debugpy
- 3.4. Install the packages for definitions
- 3.5. Build the project for Docker
- 3.6. Create the debug file
- 3.7. Create the launch.json file to allow VSCode to attach to our application
- 3.8. Create a test view
- 4. Debugging your code
- 5. Debugging library code
- 6. Conclusion
- 7. References
1. Requirements
- Docker Engine
- Docker Compose v2.23.3 (v2.24.1 has a bug when using two docker-compose.yml files that override)
- Python version manager pyenv
- Python >= 3.8 (for this example, 3.11.x is used) through
pyenv
- Package manager Poetry v1.4.2
- VSCode editor at least v1.85
- Python package debugpy >= 1.8
- For this demonstration, this project will be used: https://github.com/wiamsuri/django-gunicorn-nginx-docker
2. Motivation
Many tutorials available online don't acknowledge the problems of debbuging Django applicants with Docker, usually they explain basic functioning and sometimes are just as vague on how it works. My intention is explaining how to set up a debugger for Django applications that use Docker, Poetry and VSCode, correctly.
One main problem that I constantly see is the debugpy module always being installed when executing the container for debugging, which consumes bandwidth and time. It should be installed as a development dependency for the project, so you can always use it when needed.
3. How to set up for this example
If you already have an project configured with pyenv
, Poetry and Docker. Skip steps 1, 2 and 8.
3.1. Clone the project and have some requirements ready
Install the pyenv
Python version manager, there are various ways to do it here. My personal choice is the automatic installer. Also check the shell set up, it will enable your environment to always have pyenv
easily from your terminal.
After that, install the latest Python version available of 3.11
, do it using the following:
pyenv install 3.11
As soon as you installed the specific Python version, install the Poetry package manager. I recommend using the official installer, once done, proceed to clone the repository.
Clone the project using Git:
git clone https://github.com/wiamsuri/django-gunicorn-nginx-docker
You will need to specify the Python version to 3.11.x
at the root of the project:
pyenv local 3.11.<hotfix_version_installed>
At this point, I'm assuming you also have the Docker Engine and Docker Compose V2 installed.
3.2. Enable virtual environment
Enable the virtual environment. If you don't have one, it will create for you with the command below:
poetry shell
3.3. Add debugpy
debugpy
description from it's repository:
An implementation of the Debug Adapter Protocol for Python 3.
It is a module maintained by Microsoft in order to debug Python programs, allowing connection to a debugger.
If the project does not have the debugpy
module, add it with the command below:
poetry add --group=dev debugpy
Done this way, debugpy
will always be available at development and you won't consume bandwidth and lose your time waiting for the debugger module to install everytime.
3.4. Install the packages for definitions
The command below will install all the packages needed for the project so VSCode can identify the packages in the source code. This step is needed to allow definitions for library's to be available:
poetry install
3.5. Build the project for Docker
Our project example has a README.md
file which explains how to build the project, each project has it's own configuration and explanations on how to build them. You can read it here or use the following commands sequentially:
cp django.env.example django.env
docker compose build
3.6. Create the debug file
This file is essential to allow debugging, as it will reuse all configurations defined in other files and only changes what is needed to accomplish our task.
- At the same level of
docker-compose.yml
, create a new file calleddocker-compose.debug.yml
. - It should have the service you need to debug, as also the
ports
andcommand
. - It must have the same port used for the application and debug port defined.
File example:
version: "3.7"
services:
app:
ports:
- "8000:8000"
- "5678:5678"
command: ["sh", "-c", "python3 -m debugpy --listen 0.0.0.0:5678 manage.py runserver 0.0.0.0:8000"]
As shown in the file, it's calling the debugpy
module and listening for connections at the port 5678
, but it isn't waiting the connection of the debugger, it will run the application just as normal. If you need to wait for it to connect, you might need the flag --wait-for-client
, then the application will only start when the debugger is connected. Check more information in the debugpy
module documentation.
3.7. Create the launch.json file to allow VSCode to attach to our application
In case you don't have a launch.json
file, create one. VSCode offers a template as default.
Click on create a launch.json file.
- You might need to select the debugger, choose
Python
. - Then choose the debug configuration, go for
Remote Attach
. - It will ask configurations for the remote debugging, stick with the default, or change to your needs.
The result will be something like this:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Remote Attach",
"type": "python",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
}
],
"justMyCode": true
}
]
}
Change the property justMyCode
to false, so that the debugger can have access to code from the library that your code might call to see it's behavior.
3.8. Create a test view
As the project is just an example, it has a basic template for running Django with Docker. We have to create a view to test our debugger and check if it's running correctly.
At the urls.py
file, in the config/
folder, create a hello_view
function and add it to the urlpatterns
list.
from django.contrib import admin
from django.http import HttpResponse
from django.urls import path
def hello_view(request):
if request.method == "GET":
return HttpResponse("Hello, world!")
urlpatterns = [
path('admin/', admin.site.urls),
path('hello/', hello_view)
]
With this function, the debugger will be able to stop the execution of our code and check what's happening inside the program.
4. Debugging your code
4.1. Execution of docker compose up with debug
If everything was done correctly, we can start our application with the debugging command. In order for this event to happen, specify the debug file in the command. Do it as follows:
docker compose -f docker-compose.yml -f docker-compose.debug.yml up
The Docker Compose will overlap the files and use the debug file specified. As it's using a debug file, a base file must be determined for the services. As you may have other docker-compose
files, just ensure the docker-compose.debug.yml
is the last one, so the program can overlap the files and get their configurations correctly.
4.2. Attach debugger to application
With the application running, select the "Run and Debug" tab at VSCode, it will look like this if the launch.json
file is present:
Click the play button indicated as Python: Remote Attach. In normal scenarios, it will connect the debugger to your application and a toolbar will appear on screen.
This means the debugger is working now, let's test it.
4.3. Request the URL and debug your code
Open your browser of choice and type localhost:8000/hello/
, a call to the test view will be made. The result should be a "Hello, world!":
Notice that the debugger hasn't triggered yet, that's because we didn't specified any breakpoints in the code.
Set up a breakpoint on line 22 (L22) by clicking on the left side of the line number:
Now we refresh the page and you will see the magic happening.
Our code execution stopped at the breakpoint we've just specified, you will see variables, call stack and even watch for specific values or expressions. You can learn more about the debugger screen in the VSCode documentation.
That's the basic to debug your code and see how it's behaving. However, this configuration does not allow to make breakpoints inside libraries that you might have in your project.
The next section will explain what's needed to do in order to debug libraries directly from inside your project.
5. Debugging library code
Sometimes you're not interested in debugging your code. Instead, you want to understand the library logic or behavior in order to fix your code, or the library itself.
In this scenario, we need breakpoints inside the library code. However, by default, the configuration created above (which solves most problems) isn't enough to allow debugging library code.
Under these circumstances, you will get this grey empty dot:
In order to solve this, changes are needed at the launch.json
, environment variables in .bashrc
and settings.json
to make it work.
5.1. Changing launch.json
to work with library code
To make the debugger aware that the library code is available, some modifications are needed.
To accomplish this, changes to launch.json
file must be done, so that the debugger can seek for the path were the library code is. By adding another object at pathMappings
, we make the code available to the debugger.
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Remote Attach",
"type": "python",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"pathMappings": [
{
"localRoot": "${workspaceFolder}",
"remoteRoot": "."
},
{
"localRoot": "${env:PACKAGES_ENV_PATH}/${config:python.poetry.project.venv}/lib/python3.11/site-packages/",
"remoteRoot": "/usr/local/lib/python3.11/site-packages/"
}
],
"justMyCode": false
}
]
}
With this configuration provided for the debugger, which contains the library code path, it will seek for the files and allow to set a breakpoint.
Although, I have to explain what is that object and how you can configure it.
5.2. Configuring variables for library code debug
${env:PACKAGES_ENV_PATH}
is an environment variable defined in .bashrc
(or similar) that tells us where to look for the libraries source code. This path can be obtained by using:
poetry env info --path
The result will be similiar to:
/home/youruser/.cache/pypoetry/virtualenvs/django-gunicorn-nginx-docker-ucLVPu3k-py3.11
Until virtualenvs/
, the path leads to all virtual environments that your Poetry creates. After it, is the specific virtualenv for the project.
The environment variable VSCode needs to read, must be defined at .bashrc
(or similars) as:
#VSCode debug
export PACKAGES_ENV_PATH="/home/youruser/.cache/pypoetry/virtualenvs"
After adding this piece of code to .bashrc
, VSCode must be restarted to allow the process to get the new defined env.
This process has to be done due to how VSCode is opened and how processes work, for more info, see here.
${config:python.poetry.project.venv}
is a user defined variable inside settings.json
which tells us which virtualenv the debugger must look for.
The last part of the command mentioned above can be used to fulfill this part. If the project doesn't have a settings.json
file, just create one under .vscode/
folder, in case you have it, define the new variable to use:
{
"python.poetry.project.venv": "django-gunicorn-nginx-docker-ucLVPu3k-py3.11"
}
I tried to came up with a key that won't affect the VSCode ecosystem or any extensions (that I've heard of).
This was the best way I've found that allowed to easily define the path in order to share the launch.json
file with other coworkers through source control software.
Observation: you can also hardcode the path to make your life easier, .
The Python version won't change frequently, so I fixed it in the configuration.
In this context, I'm assuming that settings.json
will never be commited to the source control, and the user can change as he desires. If that's not the case, I would recommend to hardcode the path in the localRoot
, but every developer path will be different, be aware. DO NOT FORGET to never include this change to your commits.
If you know about a better way to do this configuration for library source code debugging, let me know in the comments or reach out to me.
5.3. Debugging the library code
Commands are the same from the Debugging your code section, the only difference is where the breakpoint is indicated.
For the example, I'd choosen the WSGIHandler
class as it's purpose is receiving the requests and serving them.
I set the breakpoint at line 123 (L123). If the dot is red, it means the debugger can use the breakpoint properly. Then I refreshed the page for our test view, and this is the result:
From there, you can happily (or not) debug the code.
6. Conclusion
Debugging is not a easy task, but with this tutorial I expect that somehow I made your life easier. So you can achieve your objective and/or catch that bug that is annoying you.
Feel free to comment and warn me about any mistakes that this article may have.
Thanks for reading until here, Ferka out ✌️.
7. References
7.1 Docker Engine and Docker Compose
- https://docs.docker.com/engine/install/
- https://docs.docker.com/compose/install/linux/#install-the-plugin-manually (be aware of the v2.24.1 bug with overlap compose files)
7.2 Pyenv
- https://github.com/pyenv/pyenv
- https://github.com/pyenv/pyenv#installation
- https://github.com/pyenv/pyenv#set-up-your-shell-environment-for-pyenv
7.3 Poetry
- https://python-poetry.org/docs/#installation
- https://python-poetry.org/docs/#installing-with-the-official-installer
- https://python-poetry.org/docs/managing-environments/
7.4 Git
7.5 Project
7.6 Debugpy
- https://stackoverflow.com/questions/76876915/pydev-debugger-unable-to-find-translation-for-please-revise-your-path-mapping
- https://github.com/microsoft/vscode-python-debugger/issues/158
7.7 VSCode
- https://code.visualstudio.com/docs/editor/debugging#_launch-configurations
- https://code.visualstudio.com/docs/editor/variables-reference#_configuration-variables
- https://code.visualstudio.com/docs/editor/debugging
- https://code.visualstudio.com/docs/python/environments
- https://code.visualstudio.com/api/extension-guides/command
- https://stackoverflow.com/questions/66971452/how-to-add-virtual-environment-to-vscode-launch-json
- https://github.com/microsoft/vscode/issues/29679
- https://www.google.com/search?channel=fs&client=ubuntu-sn&q=how+to+populate+vscode+env
- https://stackoverflow.com/questions/75767757/how-can-i-specify-environment-variables-for-the-vs-code-process-in-my-vs-code-se
- https://unix.stackexchange.com/questions/38205/change-environment-of-a-running-process
- https://stackoverflow.com/questions/48595446/is-there-any-way-to-set-environment-variables-in-visual-studio-code
- https://stackoverflow.com/questions/44303316/vscode-defining-own-variables-in-tasks-json
Top comments (1)
Hi @ferkarchiloff
Thanks for this tutorial but I get an error with the docker-compose.debug.yaml file when I run
docker compose -f docker-compose.yaml -f docker-compose-debug.yaml up
, I get the error:service "app" has neither an image nor a build context specified: invalid compose project
==> a service in a compose file MUST have either an image or a build command
EDIT: so I added the python:3.12-slim image and it started.
Then it "gracefully stopped" with the error:
Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:8000 -> 0.0.0.0:0: listen tcp 0.0.0.0:8000: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
Indeed, I have ports: 8000:8000 in both compose files so it can't work...
EDIT 2: I have docker compose v2.29.2