Skip to content
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,35 @@ 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`.

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:

```
journalctl -u jupyter -u code-interpreter
systemctl status code-interpreter
```
23 changes: 23 additions & 0 deletions template/build_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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_000),
debug=True,
),
alias=alias,
cpu_count=2,
memory_mb=2048,
on_build_logs=default_build_logger(min_level="debug"),
)

print(f"Built debug template: {alias}")
35 changes: 35 additions & 0 deletions template/debug_logs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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=600)
print(f"sandbox: {sbx.sandbox_id}")

CMDS = [
"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 --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:
for cmd in CMDS:
print(f"\n===== $ {cmd} =====")
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()
5 changes: 5 additions & 0 deletions template/systemd/jupyter-debug.conf
Original file line number Diff line number Diff line change
@@ -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
31 changes: 18 additions & 13 deletions template/template.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
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: ReadyCmd | None = None,
debug: bool = False,
):
enabled_kernels = set(["python", "javascript"] + kernels)
# Start with base template
Expand Down Expand Up @@ -104,16 +106,18 @@ 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")
Comment thread
jakubno marked this conversation as resolved.
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",
)
Comment thread
mishushakov marked this conversation as resolved.
else:
template = template.copy("start-up.sh", ".jupyter/start-up.sh").run_cmd(
"chmod +x .jupyter/start-up.sh"
Expand All @@ -136,6 +140,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)
Loading