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,--reloadand--without-threads - Start the debugger with
{"type": "python", "request": "attach", "preLaunchTask": "Enable remote debug"} - Add
ptvsdcode tosite-packages/sitecustomize.pyby 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
ptvsdcode to yourFLASK_APPfile - Run your Flask app with
--no-debuggerand--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
