Debugging Django in a Docker container
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.
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 ispython
.request
can be one of 2 values -launch
orattach
. 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 likeDockerfile
,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 yourDockerfile
, but follow most tutorials or instructions and you will likely end up with it calledapp
. Make sure the value is correct for the settings in your ownDockerfile
.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 port8000
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 Dockerfile
s 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 port3000
for communication from VS code (if you chose a different port number in yourlaunch.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 hitF5
to connect it to the container (which can only be done oncedebugpy
is listening). debugpy.wait_for_client()
then waits for information from VS Code on port3000
.
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 useRUN
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 attachSTDIN
and so will not work withbreakpoint
,pdb
etc.
And that’s it - 2 ways to help you debug your application while it’s running in a docker container.
Happy debugging!