Skip to main content

actor-debugger (Python)

Drop-in remote debugging for any Apify Python Actor with a two-line Dockerfile change. It launches your Actor under debugpy (the debugger that powers VS Code's Python debugging) and serves a full browser debugger UI over the run's container URL — so you open one link in your own browser and debug. No IDE, no tunnel, no local setup, no rebuild of your source, and no browser or IDE inside the Actor.

This is the Python sibling of the Node/TS actor-debugger npm package, injected the same way:

# Get the package
RUN pip install actor-debugger

# Swap the entrypoint for the debug launcher (revert this line to disable debugging):
CMD ["python3", "-m", "actor_debugger", "--brk"]     # was e.g.: CMD ["python3", "-m", "src"]

That's the entire integration — no code changes, no new ports, no platform configuration. Build, run, and the run log prints one URL:

[actor-debugger] OPEN THIS in your local browser for a full debugger UI (no local setup):
[actor-debugger]   https://<run>.runs.apify.net/ui/

Open it: click line numbers to set breakpoints, step, inspect the call stack and variables, evaluate expressions in the paused frame, break on exceptions. --brk pauses the Actor on its first line until you attach — drop it to let the Actor run and attach mid-flight instead.

Until the package is published to PyPI, install it from the repository instead:

RUN pip install "actor-debugger @ git+https://github.com/apify/actor-debugger.git@master#subdirectory=python"

CLI forms

CMD ["python3", "-m", "actor_debugger"]              # auto-detect the Actor's entrypoint
CMD ["python3", "-m", "actor_debugger", "--brk"]     # pause on the first line until attached
CMD ["python3", "-m", "actor_debugger", "-m", "src"] # explicit module
CMD ["python3", "-m", "actor_debugger", "main.py"]   # explicit file

(actor-debugger also works as a console command; the python3 -m form is immune to PATH surprises in slim images.)

Entrypoint detection: an explicit -m <module> / <file.py> argument always wins. Otherwise the launcher scans the working directory for runnable packages (top-level directories with a __main__.py) — by shape, not by name — which covers every current Apify Python template (my_actor/), older ones (src/), and Crawlee-generated projects, whose package is named after your project. The apify/actor-python base image ships a placeholder src/ package ("replace this file with your actual application code") that exists in every derived image — it is recognized by content and skipped. With several real runnable packages, src and my_actor are preferred, otherwise the log lists the candidates and asks for an explicit -m. Flat layouts fall back to src/main.pymain.py / __main__.py / app.py. The run log always prints entrypoint: … so a wrong pick is immediately visible.

How it works

  1. It launches your Actor as python -m debugpy --listen 127.0.0.1:5678 <entry> — the debugger runs on your code, in its own process, exactly as it normally runs. debugpy speaks the Debug Adapter Protocol (DAP), the same protocol VS Code uses.
  2. It runs one HTTP server on ACTOR_WEB_SERVER_PORT that:
    • serves a browser debugger UI (~30 KB of static files bundled with the package) — the DAP client is JavaScript running in your browser;
    • bridges a WebSocket at /dap to debugpy's DAP-over-TCP socket, translating framing (one WebSocket message per DAP JSON message ↔ Content-Length-framed TCP), injecting the adapter's loopback address into attach requests, and sending a disconnect on the browser's behalf when a tab closes abruptly — which is what makes page reloads re-attach cleanly;
    • serves /source?path=... so the UI can display files that exist only on the container's disk (the same problem the Node version solves by inlining source maps).
  3. The run log prints the one URL above. Program stdout keeps flowing to the Actor run log as usual; breakpoints live in the browser's localStorage, so they survive reloads and re-attach automatically.

Because the container side is a dumb byte bridge, all protocol intelligence lives in the served frontend — the Actor image gains only debugpy (~3 MB) plus the static files. The platform is used exactly as-is: the standard container web-server port is the only channel. Prefer a raw channel? wss://<run>.runs.apify.net/dap speaks DAP directly, one JSON message per WebSocket text frame — any DAP client can drive it.

Verified on the Apify platform

The full loop has been exercised against a real platform run: the UI served over https://<run>.runs.apify.net/ui/, the wss://…/dap WebSocket passed the platform ingress, and a live run was attached, paused, and driven from a plain browser. Automated coverage in this repo: a protocol test drives a complete DAP session through the bridge (breakpoint, stack, scopes, variables, evaluate, resume, re-attach after disconnect), and a headless-Chromium test drives the real UI end to end (entry pause under --brk, gutter breakpoints, stepping, variables, in-frame evaluate, resume, re-attach after page reload), including idle sessions.

Security

The debug endpoint is unauthenticated — anyone who reaches the container URL can execute code in your run (and read its env, including APIFY_TOKEN); /source additionally serves any file readable in the container. Keep the debug CMD only in builds you are actively debugging, prefer a restricted run/token, never ship it in a published Actor, and gate the endpoint (owner-only) before any non-prototype use. This matches the security posture of the Node version; both need the same hardening pass.

Design notes: the routes considered

The constraint was: no platform/infrastructure changes, and nothing but a browser on the debugging machine. Everything must therefore run inside the Actor container and be served over the one exposed web-server port. Options considered:

Approach Verdict
debugpy + served browser DAP client (this package) CPython unchanged, actor code and deps run exactly as in production; debugpy is the canonical Python debugger; image cost ~3 MB. The UI is ours to grow. Chosen.
GraalPy --inspect — GraalVM's Python speaks the Chrome DevTools Protocol natively, so the Node version's chii DevTools frontend + CDP proxy would work unchanged Elegant symmetry, but it swaps the runtime under the Actor: a different base image, slower startup, and C-extension compatibility risk for real-world deps (lxml, pydantic, cryptography, …). Debugging a different interpreter than production undermines the point.
DAP→CDP translation shim — keep CPython + debugpy, translate DAP into the Chrome DevTools Protocol and serve the same chii DevTools frontend the Node version uses Best possible UI for free, one frontend for both languages, but the DevTools frontend is picky about Debugger/Runtime lifecycle — a meaningful project on its own. The /dap WebSocket this package exposes is where such a shim would slot in later.
code-server / openvscode-server in the container Great UX, zero custom code, but ~300 MB and a Node runtime in every Python Actor image, plus auth wiring. Overkill for "set a breakpoint in a run".
web-pdb / xterm.js + pdb over WebSocket Tiny, but a terminal pdb UX (no gutter breakpoints, no variable tree) and pdb can't attach to a running program the way debugpy can.
Jupyter server in the container (JupyterLab's debugger also speaks debugpy) The kernel model doesn't fit debugging an already-running script; heavyweight.
dap-python — a typed Python DAP client library The DAP client here is the browser; the container side is a protocol-agnostic byte bridge. Useful only for a server-orchestrated variant, at the cost of a Pydantic dependency and a Python ≥3.12 floor.

The same architecture extends beyond Python: any language with a DAP adapter (Node via js-debug, Go via Delve — which speaks DAP natively — Rust via lldb-dap, …) can sit behind the identical bridge and frontend; only the adapter spawn command and entrypoint detection differ.

Releasing

Publishing to PyPI is automated by .github/workflows/publish_to_pypi.yml, which fires on py-v* tags and publishes via PyPI Trusted Publishing (OIDC) — no API token or repository secret. The trusted publisher configured on PyPI is: project actor-debugger, repository apify/actor-debugger, workflow publish_to_pypi.yml (the workflow file name must stay exactly that). To cut a release:

# bump version in python/pyproject.toml and python/src/actor_debugger/__init__.py, commit, then:
git tag py-v0.1.0
git push --tags

The workflow first installs the package, checks the tag matches the pyproject version, and smoke tests the CLI and the served debugger UI; only then does the publish job build sdist+wheel and upload.

Notes

  • Only runtime dependency is debugpy; the HTTP server and the RFC 6455 WebSocket implementation are pure stdlib (Python ≥3.9).
  • A crashed or closed debugger tab never blocks a running Actor — the process only waits at startup, and only with --brk.
  • "Just my code" is applied on (re)connect; untick it in the UI header to step into library code.

Release files for actor-debugger 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for actor-debugger 0.1.1
File Size Uploaded
actor_debugger-0.1.1.tar.gz 20.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for actor-debugger 0.1.1
File Interpreter ABI Platform
actor_debugger-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 42.3 kB

Release files / actor_debugger-0.1.1.tar.gz

Download URL actor_debugger-0.1.1.tar.gz
Size 20.0 kB
Tags Source
SHA-256 checksum
How to use checksums
c4f35ed996ab81071452cc8da2041dcc8dd21bd5bdb92aa09055cef28a57570b
BLAKE2b-256 checksum
How to use checksums
4c3b3bd5b61611a7dbd98e49b083790962866c204b515741f4356b0c47e1182d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release files / actor_debugger-0.1.1-py3-none-any.whl

Download URL actor_debugger-0.1.1-py3-none-any.whl
Size 22.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b0c3f242e3e5f77cd84b050d36d0d15c8d092cf2ce78baf5313ca536ffddbad0
BLAKE2b-256 checksum
How to use checksums
1522ea440db14f36fa77908578c7479a4f51520ccd1cd623983d39e012a09542
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.2

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page