Quickstart
Bring up the full stack locally and run an interactive job end to end.
This walks you from a fresh clone to a working interactive execution in a few minutes. Everything runs locally against your Docker daemon — no cloud account needed.
Prerequisites
- Docker Desktop running, with cgroup v2 (required for resource limits).
- Node.js 22+ and pnpm 10+ (for the API and the stub driver).
- Go 1.26+ (for the worker).
- Ports 8080 (API) and 6001 (soketi) free on the host.
The worker mounts the host Docker socket and builds sandboxes on the host daemon — there is no Docker-in-Docker. That's why the language images must exist on your host before you run a job.
Run it
Clone and copy the environment file
git clone https://github.com/teovillanueva/code-runner.git
cd code-runner
cp .env.example .envEvery default in .env.example is safe for local development. You don't need to
change anything to get started. (See Configuration
for what each variable does.)
Build the language sandbox images
The worker launches each job inside a pre-built, language-specific image. Build all four bundled languages on your host daemon:
make build-images
# Builds: executor/python:3.12 executor/rust:1.83 executor/r:4.4 executor/sqlite:3You can also build a single image, e.g. make python-image.
Bring up the stack
docker compose up # or: make up (adds --build)This starts redis, soketi, api, and worker. The API is now live
at http://localhost:8080.
Verify it's healthy (no auth required):
curl http://localhost:8080/health
# {"status":"ok"}Run the interactive end-to-end demo
make e2eThe bundled stub is a tiny interactive driver: it submits a Python program
that prompts for a name, detects the prompt, sends World over stdin, and asserts
the round-trip. Abbreviated output:
[stub] stdout: name?
[stub] detected prompt, sending stdin: World
[stub] stdout: hello World
[stub] result: exitCode=0 reason=exit durationMs=...
[PASS] ===== E2E PASS: interactive execute hello World round-trip succeeded =====If you see E2E PASS, your stack is fully working — queue, sandbox, stdin
round-trip, and soketi output streaming all verified.
Tear down
make down # docker compose down -vSubmit a job by hand
Want to drive it yourself instead of the stub? Here's the minimal flow with curl
(see Request Lifecycle for why the order matters).
curl -X POST http://localhost:8080/v1/execute \
-H "Authorization: Bearer dev-insecure-token-change-me" \
-H "Content-Type: application/json" \
-d '{
"language": "python",
"files": [{ "name": "main.py", "content": "print(\"hello from code-runner\")\n" }]
}'
# 202 {"jobId":"<uuid>","channel":"private-run-<uuid>","status":"queued"}After your client has subscribed to the private-run-<jobId> soketi channel,
send the start signal. (For a curl-only test you can skip subscribing — output
just won't be delivered anywhere.)
curl -X POST http://localhost:8080/v1/jobs/<jobId>/start \
-H "Authorization: Bearer dev-insecure-token-change-me"
# 202 {"ok":true}curl http://localhost:8080/v1/jobs/<jobId> \
-H "Authorization: Bearer dev-insecure-token-change-me"
# 200 {"jobId":"...","state":"running","language":"python","version":"3.12",...}Multiple files & subdirectories
files[] takes more than one entry, and a name may contain / to create
subdirectories under /workspace. Here main.py reads a CSV materialized at
data/input.csv:
curl -X POST http://localhost:8080/v1/execute \
-H "Authorization: Bearer dev-insecure-token-change-me" \
-H "Content-Type: application/json" \
-d '{
"language": "python",
"files": [
{ "name": "main.py", "content": "import csv\nwith open(\"data/input.csv\") as f:\n print(sum(1 for _ in csv.reader(f)), \"rows\")\n" },
{ "name": "data/input.csv", "content": "a,b\n1,2\n3,4\n" }
]
}'Files also support binary content ("encoding":"base64") and, for large or reused
files, content-addressed blobs via ref. See
Multi-file input.
All /v1/* endpoints require Authorization: Bearer <EXECUTOR_API_TOKEN>.
The default dev token is dev-insecure-token-change-me — change it before
exposing the service to anything. Generate one with openssl rand -hex 32.
To receive output in real time, you need a client subscribed to soketi. The fastest path is the Node SDK + React integration, or read Interactive Sessions for the full end-to-end pattern.