Debug Actors on the Apify platform
You can reproduce most bugs by running your Actor locally and using your IDE's debugger. However, some bugs are related to the platform's proxies, memory limits, environment variables, or data that exists only in a real run. This guide shows how to attach a debugger to such a run on the Apify platform.
Infrastructure constraints
An Actor run is a Docker container on a shared worker machine. You can't open a TCP connection to the container, so there's no SSH and no port forwarding.
The one inbound channel is the container web server. Whatever listens on ACTOR_WEB_SERVER_PORT (default 4321) inside the container is reachable at the run's container URL, https://<run>.runs.apify.net. The platform forwards HTTP and WebSocket traffic to that port. It doesn't forward raw TCP.
Debuggers speak raw TCP. The Node.js inspector listens on port 9229, debugpy on 5678. Bridging that mismatch takes a small server inside the container, which carries the debug port over WebSocket on the web server port.
These constraints shape what debugging on the platform looks like:
- Without an access check, anyone who can reach the container URL can reach the debugger. A debugger is a code-execution channel into the run, with access to its environment, including
APIFY_TOKEN. - A run paused on a breakpoint keeps consuming compute and counts toward the run timeout. Set a generous timeout and abort the run when you finish.
- A migration restarts the container and drops the debug session.
- Breakpoints bind to the deployed code. Keep your local checkout at the same commit as the build you debug.
Attach a debugger
You have two independent options:
| Actor debugger | wstunnel | |
|---|---|---|
| Where you debug | In any browser, with no local tooling | In your local IDE |
| What you add to the image | The actor-debugger package | The wstunnel binary |
| Languages | Node.js/TypeScript, Python | Anything with a TCP debug protocol |
| Best for | A quick look at a run | A full IDE experience |
Actor debugger
The experimental actor-debugger package launches your Actor under its native debugger and serves a debugger UI on the web server port. You open one URL from the run log in your browser. Nothing runs on your machine.
- JavaScript/TypeScript
- Python
-
Install the package in your Dockerfile, after your other dependencies, so the project's
npm installdoesn't prune it:RUN npm install actor-debugger -
Replace the Dockerfile entrypoint, for example
CMD ["node", "dist/main.js"], with the debug launcher:CMD ["npx", "actor-debugger", "--brk"]The launcher finds your entrypoint from
scripts.startormaininpackage.json, or from conventional paths likedist/main.js. To name the entrypoint yourself, pass its path:CMD ["npx", "actor-debugger", "dist/main.js"]. -
Build the Actor, start a run, and open the URL from the run log in your browser:
https://<run>.runs.apify.net/devtools/js_app.html?wss=<run>.runs.apify.net/<uuid>That page is Chrome DevTools connected to your Actor. TypeScript sources show up through source maps. The launcher inlines external
.js.mapfiles at startup, so anytscbuild withsourceMaporinlineSourceMapenabled works.
-
Install the package in your Dockerfile:
RUN pip install actor-debugger -
Replace the Dockerfile entrypoint, for example
CMD ["python", "-m", "my_actor"], with the debug launcher:CMD ["python", "-m", "actor_debugger", "--brk"]The launcher finds the runnable package in the working directory, which covers the Apify Python templates. To name the entrypoint yourself, pass the module or file:
CMD ["python", "-m", "actor_debugger", "-m", "my_actor"]orCMD ["python", "-m", "actor_debugger", "main.py"]. -
Build the Actor, start a run, and open the URL from the run log in your browser:
https://<run>.runs.apify.net/ui/That page is a debugger UI for debugpy. Select a line number to set a breakpoint, then step, inspect variables, and evaluate expressions in the paused frame.
The --brk flag pauses the Actor on its first line and holds the run there until you open the debugger URL. Without the flag, the Actor starts working immediately and you can open the URL at any point during the run. To stop debugging, restore the original CMD and rebuild the Actor.
wstunnel
wstunnel tunnels TCP over WebSocket. The server runs inside the container on the web server port. The client runs on your machine and exposes the remote debug port on localhost. Your IDE attaches to localhost as if the Actor ran there. The tunnel works for any language with a TCP debug protocol.
-
Download the static wstunnel release binary in your Dockerfile:
- JavaScript/TypeScript
- Python
FROM apify/actor-node:24ARG WSTUNNEL_VERSION=10.7.1RUN wget -qO- "https://github.com/erebe/wstunnel/releases/download/v${WSTUNNEL_VERSION}/wstunnel_${WSTUNNEL_VERSION}_linux_amd64.tar.gz" \| tar -xz -C /usr/local/bin wstunnel \&& chmod +x /usr/local/bin/wstunnelFROM apify/actor-python:3.13ARG WSTUNNEL_VERSION=10.7.1RUN curl -fsSL "https://github.com/erebe/wstunnel/releases/download/v${WSTUNNEL_VERSION}/wstunnel_${WSTUNNEL_VERSION}_linux_amd64.tar.gz" \| tar -xz -C /usr/local/bin wstunnel \&& chmod +x /usr/local/bin/wstunnelAdd
debugpyto yourrequirements.txt. -
Define a
DEBUG_SECRETenvironment variable in the Actor version and mark it as secret. wstunnel accepts only WebSocket upgrades whose path starts with this value, which keeps random visitors of the container URL out. -
Replace the
CMDin your Dockerfile, so the container starts the tunnel server and the Actor under its debugger:- JavaScript/TypeScript
- Python
CMD ["sh", "-c", ": \"${DEBUG_SECRET:?}\"; wstunnel server --restrict-to 127.0.0.1:9229 --restrict-http-upgrade-path-prefix \"$DEBUG_SECRET\" \"ws://0.0.0.0:$ACTOR_WEB_SERVER_PORT\" & exec node --inspect-brk=127.0.0.1:9229 dist/main.js"]Replace
dist/main.jswith your Actor's entrypoint. The--inspect-brkflag pauses the Actor on its first line until a debugger attaches. Use--inspectinstead to let the Actor start working immediately.CMD ["sh", "-c", ": \"${DEBUG_SECRET:?}\"; wstunnel server --restrict-to 127.0.0.1:5678 --restrict-http-upgrade-path-prefix \"$DEBUG_SECRET\" \"ws://0.0.0.0:$ACTOR_WEB_SERVER_PORT\" & exec python -m debugpy --listen 127.0.0.1:5678 --wait-for-client -m my_actor"]Replace
my_actorwith your Actor's package. The--wait-for-clientflag pauses the Actor on its first line until a debugger attaches. Omit the flag to let the Actor start working immediately.The
: "${DEBUG_SECRET:?}"guard fails the run before anything starts when the variable is unset or empty, because an empty prefix lets in any client. The--restrict-tooption limits the tunnel to the debug port, so nothing else in the container becomes reachable. Theexeccommand keeps the Actor as the main process, so it still receives the platform's shutdown signals. -
Build the Actor, start a run, and copy the container URL from the run detail page in Apify Console.
-
Install wstunnel on your machine with
brew install wstunnelor download a release binary. Then open the tunnel, and leave the command running:wstunnel client --http-upgrade-path-prefix <DEBUG_SECRET> -L tcp://9229:127.0.0.1:9229 wss://<run>.runs.apify.netUse
5678in place of both occurrences of9229for Python. The port onlocalhostnow leads to the debugger inside the run. -
Attach your IDE to
localhostand map your project root to/usr/src/app, the working directory in the Apify base images:- JavaScript/TypeScript
- Python
{"type": "node","request": "attach","name": "Attach to Apify run","address": "localhost","port": 9229,"localRoot": "${workspaceFolder}","remoteRoot": "/usr/src/app"}In JetBrains IDEs, create an Attach to Node.js/Chrome run configuration for
localhost:9229instead, and map the project root to/usr/src/appunder Remote URLs of local files. In Chrome, openchrome://inspectand addlocalhost:9229as a target.{"type": "debugpy","request": "attach","name": "Attach to Apify run","connect": { "host": "localhost", "port": 5678 },"pathMappings": [{ "localRoot": "${workspaceFolder}", "remoteRoot": "/usr/src/app" }]}In PyCharm 2026.1 or later, create an Attach to DAP run configuration for
localhost:5678instead, with the same path mapping. -
Set a breakpoint in your source files and start the configuration. The run resumes under your debugger.
You can also start wstunnel server from your Actor code and gate it on an input field. That approach avoids a separate debug build at the cost of shipping the binary in every build.
Keep debugging out of production
Both options turn the run's container URL into a code-execution endpoint. The Actor debugger has no authentication, and with wstunnel, anyone who learns the secret gets the same access.
A build with a debug entrypoint gives its users access to your Actor's environment, including APIFY_TOKEN. Keep such builds out of Apify Store.
Limit the exposure while you debug:
- Keep the debug
CMDin a dedicated Actor version with its own build tag. Production builds keep their normal entrypoint. - Run debug builds with limited permissions where the Actor allows it.
- Abort the run when you finish.