From 6fdaeb642e6477f7847eac8da590632688f8c1e2 Mon Sep 17 00:00:00 2001 From: abosio Date: Mon, 6 Jul 2026 17:09:10 -0400 Subject: [PATCH 1/3] feat: add VSCode debugger support #180 Enables interactive VSCode debugging of the containerized Django app, alongside the existing PyCharm option, over debugpy. Debugging remains off by default and is opt-in per IDE. - forward the debug port into the pod for local dev - ship a ready-to-use editor attach configuration - keep that config tracked in generated projects - document the per-IDE workflow and its trade-offs --- template/.gitignore | 3 +- template/.vscode/launch.json | 15 +++++++ template/Tiltfile | 2 +- .../utils/debugger.py | 19 +++++++- template/docs/debug.md | 43 +++++++++++++++++-- template/k8s/base/app.configmap.yaml | 2 +- template/k8s/base/django.yaml | 2 + 7 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 template/.vscode/launch.json diff --git a/template/.gitignore b/template/.gitignore index c8d82e0..73c98d4 100644 --- a/template/.gitignore +++ b/template/.gitignore @@ -56,7 +56,8 @@ env .envrc.local # vs code -.vscode/ +.vscode/* +!.vscode/launch.json .history/ # terraform files diff --git a/template/.vscode/launch.json b/template/.vscode/launch.json new file mode 100644 index 0000000..8fa74c4 --- /dev/null +++ b/template/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Django (debugpy in k8s)", + "type": "debugpy", + "request": "attach", + "connect": { "host": "127.0.0.1", "port": 5678 }, + "pathMappings": [ + { "localRoot": "${workspaceFolder}/backend", "remoteRoot": "/app/src" } + ], + "justMyCode": false + } + ] +} diff --git a/template/Tiltfile b/template/Tiltfile index 98f0a07..eada5f5 100644 --- a/template/Tiltfile +++ b/template/Tiltfile @@ -55,7 +55,7 @@ syncback( {% if copier__create_nextjs_frontend %} k8s_resource(workload='frontend', port_forwards=3000) {% endif %} -k8s_resource(workload='backend', port_forwards=8000) +k8s_resource(workload='backend', port_forwards=[8000, 5678]) k8s_resource(workload='mailhog', port_forwards=8025) k8s_resource(workload='postgres', port_forwards=5432) {% if copier__use_celery %} diff --git a/template/backend/{{copier__project_slug}}/utils/debugger.py b/template/backend/{{copier__project_slug}}/utils/debugger.py index f47c098..a0afd4b 100644 --- a/template/backend/{{copier__project_slug}}/utils/debugger.py +++ b/template/backend/{{copier__project_slug}}/utils/debugger.py @@ -19,4 +19,21 @@ def pycharm_debugger(): def vscode_debugger(): - raise NotImplementedError("VSCode debugger not implemented") + logger.info("VSCode debugpy starting...") + import debugpy + + # debugpy.listen() may only be called once per process; breakpoint() can fire + # multiple times, and runserver's reloader runs request code in the RUN_MAIN + # worker. Guard against re-listen; wait for the IDE to attach before pausing. + if not debugpy.is_client_connected(): + try: + debugpy.listen(("0.0.0.0", 5678)) + logger.info( + "debugpy listening on 0.0.0.0:5678 - waiting for VSCode to attach..." + ) + except RuntimeError: + logger.info("debugpy already listening; waiting for client.") + debugpy.wait_for_client() + logger.info("VSCode debugger attached.") + + debugpy.breakpoint() diff --git a/template/docs/debug.md b/template/docs/debug.md index 96ea6e7..9657970 100644 --- a/template/docs/debug.md +++ b/template/docs/debug.md @@ -1,6 +1,6 @@ # :bug: How to debug the application -The steps below describe how to set up interactive debugging with PyCharm. +The steps below describe how to set up interactive debugging with PyCharm or VSCode. ## PyCharm Debugging Setup Update `k8s/base/app.configmap.yaml` with `data` field `PYTHONBREAKPOINT: "utils.pycharm_debugger"` @@ -19,11 +19,48 @@ In PyCharm: ![debug__debug_configuration.png](images/debug__debug_configuration.png) +## VSCode Debugging Setup + +VSCode uses [debugpy](https://github.com/microsoft/debugpy) (already in `requirements/local.in`). +Unlike PyCharm, the pod runs the debug *server* and VSCode *attaches* to it, so Tilt +forwards the debug port (`5678`) into the pod for you. + +1. Set `PYTHONBREAKPOINT: "{{ copier__project_slug }}.utils.vscode_debugger"` in the + `data` field of `k8s/base/app.configmap.yaml`. +2. Run `tilt up`. Confirm the `backend` resource forwards both `8000` and `5678`. +3. A `.vscode/launch.json` with an "Attach to Django (debugpy in k8s)" configuration is + already included at the project root. + ## Debugging in development -Before the code you want to debug, add the following: + +`PYTHONBREAKPOINT` (set above) routes Python's built-in `breakpoint()` to the debugger for +your IDE. Before the code you want to inspect, add: ```python breakpoint() ``` -You must then set break points in your IDE and call the code as usual to hit them. +Then call the code as usual (e.g. hit the endpoint). What happens when `breakpoint()` is +reached differs by IDE: + +### PyCharm + +Hitting `breakpoint()` only *connects* the debugger — it does **not** suspend on that line +(`suspend=False`). You must set break points in the PyCharm gutter at the lines you want to +stop on; execution pauses when it reaches one of those. + +### VSCode + +`breakpoint()` **is** the stop point — you do not need to set gutter breakpoints: + +1. Add `breakpoint()` and trigger the code. The request **blocks** at that line waiting for + a debugger to attach (debugpy starts listening on the first `breakpoint()` that is hit). +2. In VSCode, open the Run and Debug panel and start "Attach to Django (debugpy in k8s)". + Execution resumes and pauses right at your `breakpoint()`. + +Once attached you *may* optionally set additional gutter breakpoints in VSCode and they'll be +hit on subsequent runs, but a plain `breakpoint()` is sufficient on its own. + +> :warning: Because the request blocks until you attach, a `breakpoint()` you never attach +> to will hang that request indefinitely. This is dev-only (local readiness/liveness probes +> are disabled), but remember to remove `breakpoint()` and reset `PYTHONBREAKPOINT: ""` when done. diff --git a/template/k8s/base/app.configmap.yaml b/template/k8s/base/app.configmap.yaml index 685be10..01585ae 100644 --- a/template/k8s/base/app.configmap.yaml +++ b/template/k8s/base/app.configmap.yaml @@ -22,7 +22,7 @@ data: # Also probably need to add cython to local.in in that case. PYDEVD_USE_CYTHON: "NO" PYDEVD_USE_FRAME_EVAL: "NO" - PYTHONBREAKPOINT: "" # "utils.pycharm_debugger" for pycharm + PYTHONBREAKPOINT: "" # "utils.pycharm_debugger" for pycharm, "{{ copier__project_slug }}.utils.vscode_debugger" for vscode {%- if copier__mail_service == 'Mailgun' %} MAILGUN_DOMAIN: "{{ copier__domain_name }}" MAILGUN_API_URL: "https://api.mailgun.net/v3"{%- endif %} diff --git a/template/k8s/base/django.yaml b/template/k8s/base/django.yaml index f445f95..01b4163 100644 --- a/template/k8s/base/django.yaml +++ b/template/k8s/base/django.yaml @@ -36,6 +36,8 @@ spec: ports: - name: http-server containerPort: 8000 + - name: debugpy + containerPort: 5678 envFrom: - configMapRef: name: app-config From 319e9d62f0add555cbc9a32913dcbfd1f4e903d2 Mon Sep 17 00:00:00 2001 From: abosio Date: Mon, 6 Jul 2026 20:48:39 -0400 Subject: [PATCH 2/3] fix: keep debugger config local-only #180 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debugger settings previously lived in the shared base config, which sandbox and prod inherit — an enabled breakpoint accidentally committed there could reach production. Moving them to the local-only overlay removes that risk while keeping the same opt-in toggle. --- template/docs/debug.md | 5 +++-- template/k8s/base/app.configmap.yaml | 8 -------- template/k8s/base/django.yaml | 2 -- template/k8s/local/kustomization.yaml | 20 ++++++++++++++++++++ 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/template/docs/debug.md b/template/docs/debug.md index 9657970..5fce8a0 100644 --- a/template/docs/debug.md +++ b/template/docs/debug.md @@ -3,7 +3,8 @@ The steps below describe how to set up interactive debugging with PyCharm or VSCode. ## PyCharm Debugging Setup -Update `k8s/base/app.configmap.yaml` with `data` field `PYTHONBREAKPOINT: "utils.pycharm_debugger"` +Set `PYTHONBREAKPOINT: "utils.pycharm_debugger"` in the `app-config` patch in +`k8s/local/kustomization.yaml` (debugger config is local-only so it can't reach sandbox/prod). In PyCharm: @@ -26,7 +27,7 @@ Unlike PyCharm, the pod runs the debug *server* and VSCode *attaches* to it, so forwards the debug port (`5678`) into the pod for you. 1. Set `PYTHONBREAKPOINT: "{{ copier__project_slug }}.utils.vscode_debugger"` in the - `data` field of `k8s/base/app.configmap.yaml`. + `app-config` patch in `k8s/local/kustomization.yaml` (debugger config is local-only). 2. Run `tilt up`. Confirm the `backend` resource forwards both `8000` and `5678`. 3. A `.vscode/launch.json` with an "Attach to Django (debugpy in k8s)" configuration is already included at the project root. diff --git a/template/k8s/base/app.configmap.yaml b/template/k8s/base/app.configmap.yaml index 01585ae..5ca6416 100644 --- a/template/k8s/base/app.configmap.yaml +++ b/template/k8s/base/app.configmap.yaml @@ -15,14 +15,6 @@ data: POSTGRES_DB: "{{ copier__project_slug }}" POSTGRES_USER: "{{ copier__project_slug }}" REDIS_URL: "redis://redis:6379/1" - # todo: see if PYDEVD_USE_* can be removed later, I was getting this error - # with pydevd-pycharm==243.26053.29: - # cannot import name 'trace_dispatch' from '_pydevd_bundle.pydevd_trace_dispatch' - # (/app/lib/python3.12/site-packages/_pydevd_bundle/pydevd_trace_dispatch.py) - # Also probably need to add cython to local.in in that case. - PYDEVD_USE_CYTHON: "NO" - PYDEVD_USE_FRAME_EVAL: "NO" - PYTHONBREAKPOINT: "" # "utils.pycharm_debugger" for pycharm, "{{ copier__project_slug }}.utils.vscode_debugger" for vscode {%- if copier__mail_service == 'Mailgun' %} MAILGUN_DOMAIN: "{{ copier__domain_name }}" MAILGUN_API_URL: "https://api.mailgun.net/v3"{%- endif %} diff --git a/template/k8s/base/django.yaml b/template/k8s/base/django.yaml index 01b4163..f445f95 100644 --- a/template/k8s/base/django.yaml +++ b/template/k8s/base/django.yaml @@ -36,8 +36,6 @@ spec: ports: - name: http-server containerPort: 8000 - - name: debugpy - containerPort: 5678 envFrom: - configMapRef: name: app-config diff --git a/template/k8s/local/kustomization.yaml b/template/k8s/local/kustomization.yaml index f589994..e70b625 100644 --- a/template/k8s/local/kustomization.yaml +++ b/template/k8s/local/kustomization.yaml @@ -29,9 +29,29 @@ patches: value: - name: DOCKER_GATEWAY_IP # used for debugger to reach out to host machine value: "172.17.0.1" + - op: add + path: /spec/template/spec/containers/0/ports/- + value: + name: debugpy + containerPort: 5678 target: kind: Deployment name: backend +# Debugger config is local-only so it can never reach sandbox/prod. +# To debug, set PYTHONBREAKPOINT below; leave it "" to disable. +- patch: |- + - op: add + path: /data/PYTHONBREAKPOINT + value: "" # "utils.pycharm_debugger" for pycharm, "{{ copier__project_slug }}.utils.vscode_debugger" for vscode + - op: add + path: /data/PYDEVD_USE_CYTHON + value: "NO" + - op: add + path: /data/PYDEVD_USE_FRAME_EVAL + value: "NO" + target: + kind: ConfigMap + name: app-config {%- if copier__use_celery %} - patch: |- - op: add From 38cb8f713b94845a014b921dade5280e52684ce2 Mon Sep 17 00:00:00 2001 From: abosio Date: Mon, 6 Jul 2026 21:13:17 -0400 Subject: [PATCH 3/3] fix: satisfy bandit B104 on debug bind #180 The dev-only debug server must bind all interfaces so the port-forward can reach the pod; scope the security check suppression to that line with a rationale. --- template/backend/{{copier__project_slug}}/utils/debugger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/template/backend/{{copier__project_slug}}/utils/debugger.py b/template/backend/{{copier__project_slug}}/utils/debugger.py index a0afd4b..02bed8f 100644 --- a/template/backend/{{copier__project_slug}}/utils/debugger.py +++ b/template/backend/{{copier__project_slug}}/utils/debugger.py @@ -27,7 +27,9 @@ def vscode_debugger(): # worker. Guard against re-listen; wait for the IDE to attach before pausing. if not debugpy.is_client_connected(): try: - debugpy.listen(("0.0.0.0", 5678)) + # Bind all interfaces so the port-forward can reach the pod; this + # debug server is dev-only (active only when PYTHONBREAKPOINT is set). + debugpy.listen(("0.0.0.0", 5678)) # nosec B104 logger.info( "debugpy listening on 0.0.0.0:5678 - waiting for VSCode to attach..." )