{"id":519,"date":"2018-02-24T19:19:15","date_gmt":"2018-02-24T11:19:15","guid":{"rendered":"https:\/\/vinta.ws\/code\/?p=519"},"modified":"2026-03-17T01:17:35","modified_gmt":"2026-03-16T17:17:35","slug":"remotely-debug-a-python-app-inside-a-docker-container-in-visual-studio-code","status":"publish","type":"post","link":"https:\/\/vinta.ws\/code\/remotely-debug-a-python-app-inside-a-docker-container-in-visual-studio-code.html","title":{"rendered":"Remotely debug a Python app inside a Docker container in Visual Studio Code"},"content":{"rendered":"<p>Visual Studio Code with Python extension has &quot;Remote Debugging&quot; 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 &quot;Step Into&quot; functionality is your good friend.<\/p>\n<p>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.<\/p>\n<p>ref:<br \/>\n<a href=\"https:\/\/code.visualstudio.com\/docs\/editor\/debugging\">https:\/\/code.visualstudio.com\/docs\/editor\/debugging<\/a><br \/>\n<a href=\"https:\/\/code.visualstudio.com\/docs\/python\/debugging#_remote-debugging\">https:\/\/code.visualstudio.com\/docs\/python\/debugging#_remote-debugging<\/a><\/p>\n<h2>Install<\/h2>\n<p>On both host OS and the container, install <code>ptvsd<\/code>.<\/p>\n<pre class=\"line-numbers\"><code class=\"language-console\">$ pip3 install -U ptvsd<\/code><\/pre>\n<p>2018.10.22 updated:<\/p>\n<p>Visual Studio Code supports <code>ptvsd 4<\/code> now!<\/p>\n<p>ref:<br \/>\n<a href=\"https:\/\/github.com\/Microsoft\/ptvsd\">https:\/\/github.com\/Microsoft\/ptvsd<\/a><\/p>\n<h2>Prepare<\/h2>\n<p>There are some materials and configurations. Assuming that you have a Dockerized Python Flask application  like the following:<\/p>\n<pre class=\"line-numbers\"><code class=\"language-dockerfile\"># Dockerfile\nFROM python:3.6.6-alpine3.7 AS builder\n\nWORKDIR \/usr\/src\/app\/\n\nRUN apk add --no-cache --virtual .build-deps \n    build-base \n    openjpeg-dev \n    openssl-dev \n    zlib-dev\n\nCOPY requirements.txt .\nRUN pip install --user -r requirements.txt\n\nFROM python:3.6.6-alpine3.7 AS runner\n\nENV PATH=$PATH:\/root\/.local\/bin\nENV FLASK_APP=app.py\n\nWORKDIR \/usr\/src\/app\/\n\nRUN apk add --no-cache --virtual .run-deps \n    openjpeg \n    openssl\n\nEXPOSE 8000\/tcp\n\nCOPY --from=builder \/root\/.local\/ \/root\/.local\/\nCOPY . .\n\nCMD [\"flask\", \"run\"]<\/code><\/pre>\n<pre class=\"line-numbers\"><code class=\"language-yaml\"># docker-compose.yml\nversion: '3'\nservices:\n    db:\n        image: mongo:3.6\n        ports:\n            - \"27017:27017\"\n        volumes:\n            - ..\/data\/mongodb:\/data\/db\n    cache:\n        image: redis:4.0\n        ports:\n            - \"6379:6379\"\n    web:\n        build: .\n        command: .docker-assets\/start-web.sh\n        ports:\n            - \"3000:3000\"\n            - \"8000:8000\"\n        volumes:\n            - .:\/usr\/src\/app\n            - ..\/vendors:\/root\/.local\n        depends_on:\n            - db\n            - cache<\/code><\/pre>\n<h2>Usage<\/h2>\n<h3>Method 1: Debug with <code>--no-debugger<\/code>, <code>--reload<\/code> and <code>--without-threads<\/code><\/h3>\n<p>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 <code>--reload<\/code> is not fully compatible with Remote Debugging.<\/p>\n<p>We put <code>ptvsd<\/code> code to <code>sitecustomize.py<\/code>, as a result, <code>ptvsd<\/code> will run every time auto-reloading is triggered.<\/p>\n<p>Steps:<\/p>\n<ol>\n<li>Set breakpoints<\/li>\n<li>Run your Flask app with <code>--no-debugger<\/code>, <code>--reload<\/code> and <code>--without-threads<\/code><\/li>\n<li>Start the debugger with <code>{&quot;type&quot;: &quot;python&quot;, &quot;request&quot;: &quot;attach&quot;, &quot;preLaunchTask&quot;: &quot;Enable remote debug&quot;}<\/code><\/li>\n<li>Add <code>ptvsd<\/code> code to <code>site-packages\/sitecustomize.py<\/code> by the pre-launch task automatically<\/li>\n<li>Click &quot;Debug Anyway&quot; button<\/li>\n<li>Access the part of code that contains breakpoints<\/li>\n<\/ol>\n<pre class=\"line-numbers\"><code class=\"language-py\"># site-packages\/sitecustomize.py\ntry:\n    import socket\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.close()\n    import ptvsd\n    ptvsd.enable_attach('my_secret', address=('0.0.0.0', 3000))\n    print('ptvsd is started')\n    # ptvsd.wait_for_attach()\n    # print('debugger is attached')\nexcept OSError as exc:\n    print(exc)<\/code><\/pre>\n<p>ref:<br \/>\n<a href=\"https:\/\/docs.python.org\/3\/library\/site.html\">https:\/\/docs.python.org\/3\/library\/site.html<\/a><\/p>\n<pre class=\"line-numbers\"><code class=\"language-shell\"># .docker-assets\/start-web.sh\nrm -f \/root\/.local\/lib\/python3.6\/site-packages\/sitecustomize.py\npip3 install --user -r requirements.txt ptvsd\npython -m flask run -h 0.0.0.0 -p 8000 --no-debugger --reload --without-threads<\/code><\/pre>\n<pre class=\"line-numbers\"><code class=\"language-json\">\/\/ .vscode\/tasks.json\n{\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Enable remote debug\",\n            \"type\": \"shell\",\n            \"isBackground\": true,\n            \"command\": \" docker cp sitecustomize.py project_web_1:\/root\/.local\/lib\/python3.6\/site-packages\/sitecustomize.py\"\n        }\n    ]\n}<\/code><\/pre>\n<p>ref:<br \/>\n<a href=\"https:\/\/code.visualstudio.com\/docs\/editor\/tasks\">https:\/\/code.visualstudio.com\/docs\/editor\/tasks<\/a><\/p>\n<pre class=\"line-numbers\"><code class=\"language-json\">\/\/ .vscode\/launch.json\n{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Python: Attach\",\n            \"type\": \"python\",\n            \"request\": \"attach\",\n            \"localRoot\": \"${workspaceFolder}\",\n            \"remoteRoot\": \"\/usr\/src\/app\",\n            \"port\": 3000,\n            \"secret\": \"my_secret\",\n            \"host\": \"localhost\",\n            \"preLaunchTask\": \"Enable remote debug\"\n        }\n    ]\n}<\/code><\/pre>\n<p>ref:<br \/>\n<a href=\"https:\/\/code.visualstudio.com\/docs\/editor\/debugging#_launch-configurations\">https:\/\/code.visualstudio.com\/docs\/editor\/debugging#_launch-configurations<\/a><\/p>\n<h3>Method 2: Debug with <code>--no-debugger<\/code> and <code>--no-reload<\/code><\/h3>\n<p>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.<\/p>\n<p>Steps:<\/p>\n<ol>\n<li>Set breakpoints<\/li>\n<li>Add <code>ptvsd<\/code> code to your <code>FLASK_APP<\/code> file<\/li>\n<li>Run your Flask app with <code>--no-debugger<\/code> and <code>--no-reload<\/code><\/li>\n<li>Start the debugger with <code>{&quot;type&quot;: &quot;python&quot;, &quot;request&quot;: &quot;attach&quot;}<\/code><\/li>\n<li>Access the part of code that contains breakpoints<\/li>\n<\/ol>\n<pre class=\"line-numbers\"><code class=\"language-py\"># in app.py\nimport ptvsd\nptvsd.enable_attach('my_secret', address=('0.0.0.0', 3000))\nprint('ptvsd is started')\n# ptvsd.wait_for_attach()\n# print('debugger is attached')<\/code><\/pre>\n<p>ref:<br \/>\n<a href=\"http:\/\/ramkulkarni.com\/blog\/debugging-django-project-in-docker\/\">http:\/\/ramkulkarni.com\/blog\/debugging-django-project-in-docker\/<\/a><\/p>\n<pre class=\"line-numbers\"><code class=\"language-shell\"># .docker-assets\/start-web.sh\npip3 install --user -r requirements.txt ptvsd\npython -m flask run -h 0.0.0.0 -p 8000 --no-debugger --no-reload<\/code><\/pre>\n<pre class=\"line-numbers\"><code class=\"language-json\">\/\/ .vscode\/launch.json\n{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Python: Attach\",\n            \"type\": \"python\",\n            \"request\": \"attach\",\n            \"localRoot\": \"${workspaceFolder}\",\n            \"remoteRoot\": \"\/usr\/src\/app\",\n            \"port\": 3000,\n            \"secret\": \"my_secret\",\n            \"host\": \"localhost\"\n        }\n    ]\n}<\/code><\/pre>\n<h3>Method 3: Just don't use Remote Debugging, Run Debugger locally<\/h3>\n<p>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 <code>127.0.0.1<\/code>. Therefore, you could just use VS Code's debugger without strange tricks.<\/p>\n<p>In practice, it is okay that your local development environment is different from the production environment.<\/p>\n<pre class=\"line-numbers\"><code class=\"language-yaml\"># docker-compose.yml\nversion: '3'\nservices:\n    db:\n        image: mongo:3.6\n        ports:\n            - \"27017:27017\"\n        volumes:\n            - mongo-volume:\/data\/db\n    cache:\n        image: redis:4.0\n        ports:\n            - \"6379:6379\"<\/code><\/pre>\n<pre class=\"line-numbers\"><code class=\"language-json\">\/\/ .vscode\/launch.json\n{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"Python: Flask\",\n            \"type\": \"python\",\n            \"request\": \"launch\",\n            \"module\": \"flask\",\n            \"console\": \"none\",\n            \"pythonPath\": \"${config:python.pythonPath}\",\n            \"cwd\": \"${workspaceFolder}\",\n            \"envFile\": \"${workspaceFolder}\/.env\",\n            \"args\": [\n                \"run\",\n                \"--host=0.0.0.0\",\n                \"--port=8000\",\n                \"--no-debugger\",\n                \"--no-reload\"\n            ],\n            \"jinja\": true,\n            \"gevent\": false\n        }\n    ]\n}<\/code><\/pre>\n<p>Sadly, you cannot use <code>--reload<\/code> 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 <code>Makefile<\/code> for running Flask app and Celery worker on macOS: <code>make run_web<\/code> and <code>make run_worker<\/code>.<\/p>\n<pre class=\"line-numbers\"><code class=\"language-makefile\"># Makefile\ninstall:\n    pipenv install\n    pipenv run pip install -ptvsd==4.1.1\n    pipenv run pip install git+https:\/\/github.com\/gorakhargosh\/watchdog.git\n\nshell:\n    pipenv run python -m flask shell\n\nrun_web:\n    pipenv run python -m flask run -h 0.0.0.0 -p 8000 --debugger --reload\n\nrun_worker:\n    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<\/code><\/pre>\n<h2>Bonus<\/h2>\n<p>You should try enabling <code>debug.inlineValues<\/code> which shows variable values inline in editor while debugging. It's awesome!<\/p>\n<pre class=\"line-numbers\"><code class=\"language-json\">\/\/ settings.json\n{\n    \"debug.inlineValues\": true\n}<\/code><\/pre>\n<p>ref:<br \/>\n<a href=\"https:\/\/code.visualstudio.com\/updates\/v1_9#_inline-variable-values-in-source-code\">https:\/\/code.visualstudio.com\/updates\/v1_9#_inline-variable-values-in-source-code<\/a><\/p>\n<h2>Issues<\/h2>\n<p>Starting the Python debugger is fucking slow<br \/>\n<a href=\"https:\/\/github.com\/Microsoft\/vscode-python\/issues\/106\">https:\/\/github.com\/Microsoft\/vscode-python\/issues\/106<\/a><\/p>\n<p>Debugging library functions won't work currently<br \/>\n<a href=\"https:\/\/github.com\/Microsoft\/vscode-python\/issues\/111\">https:\/\/github.com\/Microsoft\/vscode-python\/issues\/111<\/a><\/p>\n<p>Pylint for remote projects<br \/>\n<a href=\"https:\/\/gist.github.com\/IBestuzhev\/d022446f71267591be76fb48152175b7\">https:\/\/gist.github.com\/IBestuzhev\/d022446f71267591be76fb48152175b7<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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.<\/p>\n","protected":false},"author":1,"featured_media":520,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,116],"tags":[29,36,117,2,120],"class_list":["post-519","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-about-python","category-about-web-development","tag-debug","tag-docker","tag-editor","tag-python","tag-visual-studio-code"],"_links":{"self":[{"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/posts\/519","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/comments?post=519"}],"version-history":[{"count":0,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/posts\/519\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/media\/520"}],"wp:attachment":[{"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/media?parent=519"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/categories?post=519"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/vinta.ws\/code\/wp-json\/wp\/v2\/tags?post=519"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}