Visual Studio Code with Python extension has "Remote Debugging" feature which means you could attach to a real remote host as well as a container on localhost. NOTE: While you trace Python code, the "Step Into" functionality is your good friend.
In this article, we are going to debug a Flask app inside a local Docker container through VS Code's fancy debugger, and simultaneously we are still able to leverage Flask's auto-reloading mechanism. It should apply to other Python apps.
ref:
https://code.visualstudio.com/docs/editor/debugging
https://code.visualstudio.com/docs/python/debugging#_remote-debugging
Install
On both host OS and the container, install ptvsd
.
$ pip3 install -U ptvsd
2018.10.22 updated:
Visual Studio Code supports ptvsd 4
now!
ref:
https://github.com/Microsoft/ptvsd
Prepare
There are some materials and configurations. Assuming that you have a Dockerized Python Flask application like the following:
# Dockerfile
FROM python:3.6.6-alpine3.7 AS builder
WORKDIR /usr/src/app/
RUN apk add --no-cache --virtual .build-deps \
build-base \
openjpeg-dev \
openssl-dev \
zlib-dev
COPY requirements.txt .
RUN pip install --user -r requirements.txt
FROM python:3.6.6-alpine3.7 AS runner
ENV PATH=$PATH:/root/.local/bin
ENV FLASK_APP=app.py
WORKDIR /usr/src/app/
RUN apk add --no-cache --virtual .run-deps \
openjpeg \
openssl
EXPOSE 8000/tcp
COPY --from=builder /root/.local/ /root/.local/
COPY . .
CMD ["flask", "run"]
# docker-compose.yml
version: '3'
services:
db:
image: mongo:3.6
ports:
- "27017:27017"
volumes:
- ../data/mongodb:/data/db
cache:
image: redis:4.0
ports:
- "6379:6379"
web:
build: .
command: .docker-assets/start-web.sh
ports:
- "3000:3000"
- "8000:8000"
volumes:
- .:/usr/src/app
- ../vendors:/root/.local
depends_on:
- db
- cache
Usage
Method 1: Debug with --no-debugger
, --reload
and --without-threads
The convenient but a little fragile way: with auto-reloading enabled, you could change your source code on the fly. However, you might find that this method is much slower for the debugger to attach. It seems like --reload
is not fully compatible with Remote Debugging.
We put ptvsd
code to sitecustomize.py
, as a result, ptvsd
will run every time auto-reloading is triggered.
Steps:
- Set breakpoints
- Run your Flask app with
--no-debugger
, --reload
and --without-threads
- Start the debugger with
{"type": "python", "request": "attach", "preLaunchTask": "Enable remote debug"}
- Add
ptvsd
code to site-packages/sitecustomize.py
by the pre-launch task automatically
- Click "Debug Anyway" button
- Access the part of code contains breakpoints
# site-packages/sitecustomize.py
try:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.close()
import ptvsd
ptvsd.enable_attach('my_secret', address=('0.0.0.0', 3000))
print('ptvsd is started')
# ptvsd.wait_for_attach()
# print('debugger is attached')
except OSError as exc:
print(exc)
ref:
https://docs.python.org/3/library/site.html
# .docker-assets/start-web.sh
rm -f /root/.local/lib/python3.6/site-packages/sitecustomize.py
pip3 install --user -r requirements.txt ptvsd
python -m flask run -h 0.0.0.0 -p 8000 --no-debugger --reload --without-threads
// .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Enable remote debug",
"type": "shell",
"isBackground": true,
"command": " docker cp sitecustomize.py project_web_1:/root/.local/lib/python3.6/site-packages/sitecustomize.py"
}
]
}
ref:
https://code.visualstudio.com/docs/editor/tasks
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Attach",
"type": "python",
"request": "attach",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/usr/src/app",
"port": 3000,
"secret": "my_secret",
"host": "localhost",
"preLaunchTask": "Enable remote debug"
}
]
}
ref:
https://code.visualstudio.com/docs/editor/debugging#_launch-configurations
Method 2: Debug with --no-debugger
and --no-reload
The inconvenient but slightly reliable way: if you change any Python code, you need to restart the Flask app and re-attach debugger in Visual Studio Code.
Steps:
- Set breakpoints
- Add
ptvsd
code to your FLASK_APP
file
- Run your Flask app with
--no-debugger
and --no-reload
- Start the debugger with
{"type": "python", "request": "attach"}
- Access the part of code contains breakpoints
# in app.py
import ptvsd
ptvsd.enable_attach('my_secret', address=('0.0.0.0', 3000))
print('ptvsd is started')
# ptvsd.wait_for_attach()
# print('debugger is attached')
ref:
http://ramkulkarni.com/blog/debugging-django-project-in-docker/
# .docker-assets/start-web.sh
pip3 install --user -r requirements.txt ptvsd
python -m flask run -h 0.0.0.0 -p 8000 --no-debugger --no-reload
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Attach",
"type": "python",
"request": "attach",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/usr/src/app",
"port": 3000,
"secret": "my_secret",
"host": "localhost"
}
]
}
Method 3: Just don't use Remote Debugging, Run Debugger locally
You just run your Flask app on localhost (macOS) instead of putting it in a container. However, you could still host your database, cache server and message queue inside containers. Your Python app communicates with those services through ports which exposed to 127.0.0.1
. Therefore, you could just use VS Code's debugger without strange tricks.
In practice, it is okay that your local development environment is different from the production environment.
# docker-compose.yml
version: '3'
services:
db:
image: mongo:3.6
ports:
- "27017:27017"
volumes:
- mongo-volume:/data/db
cache:
image: redis:4.0
ports:
- "6379:6379"
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Flask",
"type": "python",
"request": "launch",
"module": "flask",
"console": "none",
"pythonPath": "${config:python.pythonPath}",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/.env",
"args": [
"run",
"--host=0.0.0.0",
"--port=8000",
"--no-debugger",
"--no-reload"
],
"jinja": true,
"gevent": false
}
]
}
Sadly, you cannot use --reload
while launching your app in the debugger. Nevertheless, most of the time you don't really need the debugger - a fast auto-reloading workflow is good enough. All you need is a Makefile
for running Flask app and Celery worker on macOS: make run_web
and make run_worker
.
# Makefile
install:
pipenv install
pipenv run pip install -ptvsd==4.1.1
pipenv run pip install git+https://github.com/gorakhargosh/watchdog.git
shell:
pipenv run python -m flask shell
run_web:
pipenv run python -m flask run -h 0.0.0.0 -p 8000 --debugger --reload
run_worker:
pipenv run watchmedo auto-restart -d . -p '*.py' -R -- celery -A app:celery worker --pid= -P gevent --without-gossip --prefetch-multiplier 1 -Ofair -l debug --purge
Bonus
You should try enabling debug.inlineValues
which shows variable values inline in editor while debugging. It's awesome!
// settings.json
{
"debug.inlineValues": true
}
ref:
https://code.visualstudio.com/updates/v1_9#_inline-variable-values-in-source-code
Issues
Starting the Python debugger is fucking slow
https://github.com/Microsoft/vscode-python/issues/106
Debugging library functions won't work currently
https://github.com/Microsoft/vscode-python/issues/111
Pylint for remote projects
https://gist.github.com/IBestuzhev/d022446f71267591be76fb48152175b7