From 72c8ba882eef0bcc6a56755e95c7f2adc33241d5 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:58:28 +0200 Subject: [PATCH 1/9] Add template debugging tooling and surface Jupyter logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Send Jupyter's stdout to the systemd journal instead of /dev/null so startup errors (e.g. failed session creation) are visible via `journalctl -u jupyter`. Add a `make debug-template` workflow (build_debug.py + debug_logs.py) that builds the template via the real systemd start path with a timeout ready-gate, then spawns a sandbox and dumps the jupyter and code-interpreter service journals — for diagnosing a server that fails its readiness check. make_template() gains an optional `ready` override to support this. Co-Authored-By: Claude Opus 4.8 --- .changeset/template-debug-tooling.md | 5 +++++ Makefile | 3 +++ template/README.md | 27 +++++++++++++++++++++++ template/build_debug.py | 19 +++++++++++++++++ template/debug_logs.py | 32 ++++++++++++++++++++++++++++ template/systemd/jupyter.service | 2 +- template/template.py | 8 ++++--- 7 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 .changeset/template-debug-tooling.md create mode 100644 template/build_debug.py create mode 100644 template/debug_logs.py diff --git a/.changeset/template-debug-tooling.md b/.changeset/template-debug-tooling.md new file mode 100644 index 00000000..0f9617cb --- /dev/null +++ b/.changeset/template-debug-tooling.md @@ -0,0 +1,5 @@ +--- +"@e2b/code-interpreter-template": patch +--- + +Improve template debuggability: send Jupyter's stdout to the systemd journal (instead of /dev/null) so startup errors are visible, and add a `make debug-template` workflow that builds via the systemd path and dumps the service journals for diagnosing a server that fails to start. diff --git a/Makefile b/Makefile index 99efa3b8..c1b25fea 100644 --- a/Makefile +++ b/Makefile @@ -3,3 +3,6 @@ start-template-server: kill-template-server: docker kill $(shell docker ps --filter expose=49999 --format {{.ID}}) + +debug-template: + cd template && python build_debug.py && python debug_logs.py diff --git a/template/README.md b/template/README.md index 19039fc4..98177ff5 100644 --- a/template/README.md +++ b/template/README.md @@ -55,3 +55,30 @@ sbx = Sandbox.create(template="code-interpreter-custom") execution = sbx.run_code("print('Hello, World!')") print(execution.logs.stdout) ``` + +## Debugging a server that won't start + +The template runs Jupyter and the code-interpreter server as **systemd** +services (`systemd/jupyter.service`, `systemd/code-interpreter.service`). This is +the path CI and production use — note it is *different* from `make +start-template-server`, which runs the Docker `start-up.sh` path. The two can +diverge, so a server that boots fine under Docker may still fail under systemd. + +When a build fails its readiness check (`Waiting for template to be ready ... +timed out`), the real cause is in the service journals. To see them: + +``` +make debug-template +``` + +This builds a debug template (gated on a fixed timeout instead of `/health`, so +it finalizes even while the server is crash-looping), spawns a sandbox, and +prints `systemctl status` + the full `journalctl` for both services. It needs +`template/.env` with your `E2B_API_KEY` and the deps from `requirements-dev.txt`. + +Inside a running sandbox you can also inspect things directly: + +``` +journalctl -u jupyter -u code-interpreter +systemctl status code-interpreter +``` diff --git a/template/build_debug.py b/template/build_debug.py new file mode 100644 index 00000000..10c17c5e --- /dev/null +++ b/template/build_debug.py @@ -0,0 +1,19 @@ +import os + +from dotenv import load_dotenv +from e2b import Template, default_build_logger, wait_for_timeout +from template import make_template + +load_dotenv() + +alias = os.getenv("E2B_DEBUG_TEMPLATE", "code-interpreter-debug") + +Template.build( + make_template(kernels=["python", "javascript"], ready=wait_for_timeout(60)), + alias=alias, + cpu_count=2, + memory_mb=2048, + on_build_logs=default_build_logger(min_level="debug"), +) + +print(f"Built debug template: {alias}") diff --git a/template/debug_logs.py b/template/debug_logs.py new file mode 100644 index 00000000..e2ef48d3 --- /dev/null +++ b/template/debug_logs.py @@ -0,0 +1,32 @@ +import os + +from dotenv import load_dotenv +from e2b import Sandbox + +load_dotenv() + +alias = os.getenv("E2B_DEBUG_TEMPLATE", "code-interpreter-debug") + +sbx = Sandbox.create(template=alias, timeout=180) +print(f"sandbox: {sbx.sandbox_id}") + +CMDS = [ + "sleep 25", # let the start command (try to) bring the services up + "systemctl --no-pager status jupyter || true", + "systemctl --no-pager status code-interpreter || true", + "journalctl --no-pager -u jupyter || true", + "journalctl --no-pager -u code-interpreter || true", + "curl -s -o /dev/null -w 'jupyter :8888 -> %{http_code}\\n' http://localhost:8888/api/status || true", + "curl -s -o /dev/null -w 'server :49999 -> %{http_code}\\n' http://localhost:49999/health || true", +] + +try: + for cmd in CMDS: + print(f"\n===== $ {cmd} =====") + result = sbx.commands.run(f"sudo bash -lc {cmd!r}", timeout=60) + if result.stdout: + print(result.stdout) + if result.stderr: + print("[stderr]", result.stderr) +finally: + sbx.kill() diff --git a/template/systemd/jupyter.service b/template/systemd/jupyter.service index 37b83f29..5ef81901 100644 --- a/template/systemd/jupyter.service +++ b/template/systemd/jupyter.service @@ -11,5 +11,5 @@ ExecStart=/usr/local/bin/jupyter server --IdentityProvider.token="" ExecStartPost=-/usr/bin/systemctl reset-failed code-interpreter Restart=on-failure RestartSec=1 -StandardOutput=null +StandardOutput=journal StandardError=journal diff --git a/template/template.py b/template/template.py index 62fec9e2..e4b57c46 100644 --- a/template/template.py +++ b/template/template.py @@ -4,6 +4,7 @@ def make_template( kernels: list[str] = ["python", "r", "javascript", "bash", "java"], is_docker: bool = False, + ready=None, ): enabled_kernels = set(["python", "javascript"] + kernels) # Start with base template @@ -136,6 +137,7 @@ def make_template( else: start_cmd = "sudo systemctl start jupyter" - return template.set_start_cmd( - start_cmd, wait_for_url("http://localhost:49999/health") - ) + if ready is None: + ready = wait_for_url("http://localhost:49999/health") + + return template.set_start_cmd(start_cmd, ready) From c78eb9b50de9bbc7131cff9d1f6669bb0ade5591 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:02:12 +0200 Subject: [PATCH 2/9] Fix wait_for_timeout units in build_debug (ms, not s) wait_for_timeout takes milliseconds (min 1000ms), so 60 collapsed to a 1s ready-gate. Use 60_000 for the intended 60s. Co-Authored-By: Claude Opus 4.8 --- template/build_debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/build_debug.py b/template/build_debug.py index 10c17c5e..b91a3729 100644 --- a/template/build_debug.py +++ b/template/build_debug.py @@ -9,7 +9,7 @@ alias = os.getenv("E2B_DEBUG_TEMPLATE", "code-interpreter-debug") Template.build( - make_template(kernels=["python", "javascript"], ready=wait_for_timeout(60)), + make_template(kernels=["python", "javascript"], ready=wait_for_timeout(60_000)), alias=alias, cpu_count=2, memory_mb=2048, From 07cc51b5e01f2e5c9bfb50d2f872edd12b7160be Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:06:43 +0200 Subject: [PATCH 3/9] Harden debug_logs against command/sandbox timeouts Address Cursor Bugbot review on PR #288: - Bump sandbox TTL 180s -> 600s so the full diagnostic sequence (sleep + per-command 60s budgets) can't outlive the sandbox. - Wrap each command in try/except so one slow or failing command no longer aborts the loop and skips the remaining journals/probes. Co-Authored-By: Claude Opus 4.8 --- template/debug_logs.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/template/debug_logs.py b/template/debug_logs.py index e2ef48d3..1304c6f6 100644 --- a/template/debug_logs.py +++ b/template/debug_logs.py @@ -7,7 +7,7 @@ alias = os.getenv("E2B_DEBUG_TEMPLATE", "code-interpreter-debug") -sbx = Sandbox.create(template=alias, timeout=180) +sbx = Sandbox.create(template=alias, timeout=600) print(f"sandbox: {sbx.sandbox_id}") CMDS = [ @@ -23,10 +23,14 @@ try: for cmd in CMDS: print(f"\n===== $ {cmd} =====") - result = sbx.commands.run(f"sudo bash -lc {cmd!r}", timeout=60) - if result.stdout: - print(result.stdout) - if result.stderr: - print("[stderr]", result.stderr) + try: + result = sbx.commands.run(f"sudo bash -lc {cmd!r}", timeout=60) + if result.stdout: + print(result.stdout) + if result.stderr: + print("[stderr]", result.stderr) + except Exception as e: + # Keep going so one slow/failed command doesn't skip the rest. + print(f"[command failed] {e}") finally: sbx.kill() From 9eebd0716af1ad87f22fd542e1fc7999fd525c9b Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:11:17 +0200 Subject: [PATCH 4/9] Drop redundant sleep in debug_logs The start command runs at build time and the resulting state is snapshotted, so a resumed sandbox already has the services running (and their journals populated). No need to wait after create. Co-Authored-By: Claude Opus 4.8 --- template/debug_logs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/template/debug_logs.py b/template/debug_logs.py index 1304c6f6..f5692f8c 100644 --- a/template/debug_logs.py +++ b/template/debug_logs.py @@ -11,7 +11,6 @@ print(f"sandbox: {sbx.sandbox_id}") CMDS = [ - "sleep 25", # let the start command (try to) bring the services up "systemctl --no-pager status jupyter || true", "systemctl --no-pager status code-interpreter || true", "journalctl --no-pager -u jupyter || true", From 9d8a701f3fb900175fc4f4fac1e60a753c513936 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:13:51 +0200 Subject: [PATCH 5/9] Type the make_template ready parameter Annotate `ready` as `ReadyCmd | None` (the type returned by wait_for_url/wait_for_timeout and accepted by set_start_cmd). Co-Authored-By: Claude Opus 4.8 --- template/template.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template/template.py b/template/template.py index e4b57c46..2c9b4e77 100644 --- a/template/template.py +++ b/template/template.py @@ -1,10 +1,10 @@ -from e2b import Template, wait_for_url +from e2b import ReadyCmd, Template, wait_for_url def make_template( kernels: list[str] = ["python", "r", "javascript", "bash", "java"], is_docker: bool = False, - ready=None, + ready: ReadyCmd | None = None, ): enabled_kernels = set(["python", "javascript"] + kernels) # Start with base template From 618c4ed21aa113149f930aa06145885de3704175 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:16:12 +0200 Subject: [PATCH 6/9] Add --max-time to debug_logs health probes A port that accepts TCP but never sends an HTTP response (the half-broken state this tool diagnoses) would otherwise hang curl until the 60s command timeout. --max-time 3 makes each probe fail fast. Co-Authored-By: Claude Opus 4.8 --- template/debug_logs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template/debug_logs.py b/template/debug_logs.py index f5692f8c..cdd7b49a 100644 --- a/template/debug_logs.py +++ b/template/debug_logs.py @@ -15,8 +15,8 @@ "systemctl --no-pager status code-interpreter || true", "journalctl --no-pager -u jupyter || true", "journalctl --no-pager -u code-interpreter || true", - "curl -s -o /dev/null -w 'jupyter :8888 -> %{http_code}\\n' http://localhost:8888/api/status || true", - "curl -s -o /dev/null -w 'server :49999 -> %{http_code}\\n' http://localhost:49999/health || true", + "curl -s --max-time 3 -o /dev/null -w 'jupyter :8888 -> %{http_code}\\n' http://localhost:8888/api/status || true", + "curl -s --max-time 3 -o /dev/null -w 'server :49999 -> %{http_code}\\n' http://localhost:49999/health || true", ] try: From f5999fbd27dec056ac818ade824a4d8b9571f9f8 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:35:02 +0200 Subject: [PATCH 7/9] Route Jupyter stdout to journal only in debug builds Keep production at StandardOutput=null. make_template(debug=True) now applies a systemd drop-in (jupyter-debug.conf) that flips Jupyter's stdout to the journal, and build_debug.py opts in. Production template behavior is unchanged. Co-Authored-By: Claude Opus 4.8 --- .changeset/template-debug-tooling.md | 2 +- template/README.md | 5 +++++ template/build_debug.py | 6 +++++- template/systemd/jupyter-debug.conf | 5 +++++ template/systemd/jupyter.service | 2 +- template/template.py | 22 +++++++++++++--------- 6 files changed, 30 insertions(+), 12 deletions(-) create mode 100644 template/systemd/jupyter-debug.conf diff --git a/.changeset/template-debug-tooling.md b/.changeset/template-debug-tooling.md index 0f9617cb..5ba52e99 100644 --- a/.changeset/template-debug-tooling.md +++ b/.changeset/template-debug-tooling.md @@ -2,4 +2,4 @@ "@e2b/code-interpreter-template": patch --- -Improve template debuggability: send Jupyter's stdout to the systemd journal (instead of /dev/null) so startup errors are visible, and add a `make debug-template` workflow that builds via the systemd path and dumps the service journals for diagnosing a server that fails to start. +Add a `make debug-template` workflow for diagnosing a server that fails to start: it builds the template via the systemd path (with a fixed-timeout ready gate and a drop-in routing Jupyter's stdout to the journal) and dumps the service journals. Production builds are unchanged. diff --git a/template/README.md b/template/README.md index 98177ff5..31f89a90 100644 --- a/template/README.md +++ b/template/README.md @@ -76,6 +76,11 @@ it finalizes even while the server is crash-looping), spawns a sandbox, and prints `systemctl status` + the full `journalctl` for both services. It needs `template/.env` with your `E2B_API_KEY` and the deps from `requirements-dev.txt`. +The debug build also applies a systemd drop-in that routes Jupyter's stdout to +the journal (`make_template(debug=True)`). Production builds keep +`StandardOutput=null`, so Jupyter's request/error logs are only captured in the +debug template. + Inside a running sandbox you can also inspect things directly: ``` diff --git a/template/build_debug.py b/template/build_debug.py index b91a3729..f4dc63b4 100644 --- a/template/build_debug.py +++ b/template/build_debug.py @@ -9,7 +9,11 @@ alias = os.getenv("E2B_DEBUG_TEMPLATE", "code-interpreter-debug") Template.build( - make_template(kernels=["python", "javascript"], ready=wait_for_timeout(60_000)), + make_template( + kernels=["python", "javascript"], + ready=wait_for_timeout(60_000), + debug=True, + ), alias=alias, cpu_count=2, memory_mb=2048, diff --git a/template/systemd/jupyter-debug.conf b/template/systemd/jupyter-debug.conf new file mode 100644 index 00000000..89377a6f --- /dev/null +++ b/template/systemd/jupyter-debug.conf @@ -0,0 +1,5 @@ +# Debug-only drop-in: route Jupyter's stdout to the journal (the base unit +# sends it to /dev/null) so ServerApp request/error logs are visible via +# `journalctl -u jupyter`. Applied only by `make_template(debug=True)`. +[Service] +StandardOutput=journal diff --git a/template/systemd/jupyter.service b/template/systemd/jupyter.service index 5ef81901..37b83f29 100644 --- a/template/systemd/jupyter.service +++ b/template/systemd/jupyter.service @@ -11,5 +11,5 @@ ExecStart=/usr/local/bin/jupyter server --IdentityProvider.token="" ExecStartPost=-/usr/bin/systemctl reset-failed code-interpreter Restart=on-failure RestartSec=1 -StandardOutput=journal +StandardOutput=null StandardError=journal diff --git a/template/template.py b/template/template.py index 2c9b4e77..9427d4bf 100644 --- a/template/template.py +++ b/template/template.py @@ -5,6 +5,7 @@ def make_template( kernels: list[str] = ["python", "r", "javascript", "bash", "java"], is_docker: bool = False, ready: ReadyCmd | None = None, + debug: bool = False, ): enabled_kernels = set(["python", "javascript"] + kernels) # Start with base template @@ -105,16 +106,19 @@ def make_template( ) if not is_docker: - template = ( - template.copy( - "systemd/jupyter.service", "/etc/systemd/system/jupyter.service" - ) - .copy( - "systemd/code-interpreter.service", - "/etc/systemd/system/code-interpreter.service", - ) - .run_cmd("systemctl daemon-reload") + template = template.copy( + "systemd/jupyter.service", "/etc/systemd/system/jupyter.service" + ).copy( + "systemd/code-interpreter.service", + "/etc/systemd/system/code-interpreter.service", ) + if debug: + # Drop-in that routes Jupyter's stdout to the journal for debugging. + template = template.copy( + "systemd/jupyter-debug.conf", + "/etc/systemd/system/jupyter.service.d/debug.conf", + ) + template = template.run_cmd("systemctl daemon-reload") else: template = template.copy("start-up.sh", ".jupyter/start-up.sh").run_cmd( "chmod +x .jupyter/start-up.sh" From 22747f27c844f447b5b65aaff68c3bd4a871af38 Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:44:49 +0200 Subject: [PATCH 8/9] Drop redundant systemctl daemon-reload from template build systemd lazily loads the freshly-copied units (and their drop-ins) on the first `systemctl start` at end of build, so the explicit daemon-reload was a no-op. Verified the prod build still reaches a healthy /health gate without it. Co-Authored-By: Claude Opus 4.8 --- template/template.py | 1 - 1 file changed, 1 deletion(-) diff --git a/template/template.py b/template/template.py index 9427d4bf..fd8dd937 100644 --- a/template/template.py +++ b/template/template.py @@ -118,7 +118,6 @@ def make_template( "systemd/jupyter-debug.conf", "/etc/systemd/system/jupyter.service.d/debug.conf", ) - template = template.run_cmd("systemctl daemon-reload") else: template = template.copy("start-up.sh", ".jupyter/start-up.sh").run_cmd( "chmod +x .jupyter/start-up.sh" From 179e18f412fc53af25c458b9726b12841045f1cc Mon Sep 17 00:00:00 2001 From: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Date: Wed, 3 Jun 2026 20:15:02 +0200 Subject: [PATCH 9/9] Drop changeset; debug tooling needs no template release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published template artifact is unchanged (jupyter.service matches main, daemon-reload removal yields an identical image, the journal drop-in only ships in debug builds). Remaining changes are the build script, dev-only scripts, and docs — no version bump warranted. Co-Authored-By: Claude Opus 4.8 --- .changeset/template-debug-tooling.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/template-debug-tooling.md diff --git a/.changeset/template-debug-tooling.md b/.changeset/template-debug-tooling.md deleted file mode 100644 index 5ba52e99..00000000 --- a/.changeset/template-debug-tooling.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@e2b/code-interpreter-template": patch ---- - -Add a `make debug-template` workflow for diagnosing a server that fails to start: it builds the template via the systemd path (with a fixed-timeout ready gate and a drop-in routing Jupyter's stdout to the journal) and dumps the service journals. Production builds are unchanged.