Rewrite code-interpreter service in Go#65
Open
jmelahman wants to merge 7 commits into
Open
Conversation
Add code-interpreter-go/, a full port of the Python FastAPI service with the same API surface (/health, /v1/execute, /v1/execute/stream SSE, /v1/files, /v1/sessions, session bash) and the same env-based configuration. Notable changes from the Python implementation: - Docker backend uses the Docker Engine API (github.com/docker/docker/client) instead of shelling out to the docker CLI; PYTHON_EXECUTOR_DOCKER_RUN_ARGS now accepts a documented flag subset and fails fast on anything else. Workspace staging/snapshot still run tar inside the container since /workspace is a tmpfs the engine copy API cannot see. - entrypoint.sh is absorbed into the binary (mode detection, cgroup v2 nesting, nested dockerd launch + SDK ping readiness); the image runs the binary directly with no shell required at runtime. - Dockerfile defaults to DHI hardened base images (overridable build args), installs the Docker daemon only for DinD builds, and drops curl/gnupg/ supervisor. SKIP_NESTED_DOCKER=1 builds now support DooD with zero docker packages. - Kubernetes backend on client-go with SPDY exec (no tar|base64 round-trip). - Fixes a keepalive unit slip (timeout_ms * 1000 seconds -> timeout_ms/1000 + 10 seconds). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Release both onyxdotapp/code-interpreter-go:<version> (with the Docker daemon; every deployment mode works, including DinD) and :<version>-slim (SKIP_NESTED_DOCKER=1, zero docker packages) so users opt OUT of the daemon rather than discovering a broken default. Add a Go CI job (gofmt/build/vet/test) plus the real-daemon Docker executor e2e test against the runner's daemon, and document the deployment modes in the README with rootless-DooD guidance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the full/-slim target definitions (tags, platforms, build args, DHI base overrides) out of the release workflow into code-interpreter-go/docker-bake.hcl so local builds and CI share one definition, and both variants build as a single graph with the common Go build stage deduplicated. The workflow now runs docker/bake-action with per-target GHA cache scopes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measured on a 24-CPU host: Go serves first request 13-25x sooner (21-30ms vs 271-763ms median), uses ~36% less idle RSS (31 vs 48 MiB), and sustains 6-35x higher API throughput under concurrency where the Python service plateaus on the GIL. Registry-measured image sizes: the published Python image is 194MB compressed vs ~45MB derived for the Go -slim variant. Execution end-to-end is not covered here (no Docker daemon available); CI's e2e test exercises that path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Building linux/arm64 on an amd64 host without binfmt registered fails with "exec format error". Pin the Go build stage to BUILDPLATFORM and cross-compile via GOOS/GOARCH so the toolchain never runs emulated, add setup-qemu-action to the release job for the runtime stage's apt layers, and document the local binfmt one-liner in the Dockerfile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace QEMU emulation in the release workflow with a native-runner matrix (ubuntu-latest / ubuntu-24.04-arm): each platform builds and pushes by digest via PUSH_BY_DIGEST, and a merge job assembles the multi-arch manifest lists with imagetools. Add `bake local` targets that build only the host's native platform and load into the local image store, plus a dhi.io registry login for the hardened bases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
code-interpreter-go/, a full Go port of the Python code-interpreter service, alongside the existing implementation (the Python service is untouched, so both can run side by side during migration).Same API surface and payloads:
GET /health,POST /v1/execute,POST /v1/execute/stream(SSEoutput/result/errorevents),/v1/filesCRUD,/v1/sessionswith TTL-enforced teardown + reaper, andPOST /v1/sessions/{id}/bash. Same environment-based configuration.Benchmarks (Python vs Go)
Measured with the committed harness (
benchmarks/, methodology and reproduction steps inbenchmarks/README.md) on a 24-CPU Linux host; two full runs, medians shown as ranges. Both services ran natively with identical config and load.The Python service plateaus under concurrency (GIL: 16-worker download throughput barely exceeds 1-worker); matching Go's 16-worker throughput would take ~6–8 uvicorn workers (~300–400 MiB RSS) vs one 43 MiB Go process.
Image sizes (compressed): the published Python image is 194 MB (registry-measured; 104 MB of that is the docker-ce layer, 45 MB the python base, ~38 MB venv/uv). The Go
-slimimage derives to ~45 MB (30 MB debian base + 13 MB gzipped binary) and the Go default to ~150 MB. Application payload: one 46 MB static binary vs 84 MB CPython + 129 MB venv.Caveat: end-to-end
/v1/executelatency is not benchmarked (needs a Docker daemon; the bench host had noCAP_SYS_ADMIN). It is dominated by container create/start, which both implementations pay identically; they differ in orchestration overhead (Python spawns ~6 docker-CLI subprocesses per execution, Go makes Engine API calls on one socket).Key design changes vs. the Python service
github.com/docker/docker/client): typed not-found/conflict errors replace stderr-string heuristics, exec-attach with demultiplexed streams feeds SSE, and image pull happens over the API. File staging and workspace snapshots still runtarinside the container over the exec API, because/workspaceis a tmpfs mount the engine's copy API writes past invisibly.PYTHON_EXECUTOR_DOCKER_RUN_ARGSis now a documented subset (--label,--env,--add-host,--dns,--ulimit) and the service fails fast at startup on unsupported flags instead of silently passing them through.PYTHON_EXECUTOR_DOCKER_BINis no longer read; the daemon connection uses standardDOCKER_HOSTet al.entrypoint.shis built into the binary (internal/bootstrap): mode detection (kubernetes / externalDOCKER_HOST/ mounted socket / DinD), cgroup v2 nesting, nesteddockerdlaunch + supervision, and readiness via SDK ping instead ofdocker info. The image runs the binary directly — no shell needed at runtime.dhi.io(override viaBUILD_IMAGE/RUNTIME_IMAGE), the Docker daemon is installed only for DinD support, andcurl/gnupg/supervisor/bashare gone.SKIP_NESTED_DOCKER=1builds support Docker-out-of-Docker with zero docker packages (the Python image needed the CLI for DooD).tar | base64round-trip.timeout_ms * 1000seconds (multi-day sleeps on a crashed API); nowtimeout_ms/1000 + 10seconds.{"detail": "<message>"}strings (not Pydantic's array format); status codes unchanged./docs//redoc//openapi.jsonare not served.Packaging & CI
docker-bake.hcl:onyxdotapp/code-interpreter-go:<version>(default, includes the Docker daemon — works in every deployment mode) and:<version>-slim(zero docker packages, for DooD/Kubernetes).docker buildx bake localbuilds the host's native platform for development.ubuntu-latest/ubuntu-24.04-arm, no QEMU), pushes by digest, and merges the multi-arch manifests withimagetools create. The Go build stage cross-compiles from$BUILDPLATFORM.golang-checksCI job: gofmt / build / vet / unit tests, plus the real-daemon Docker executor e2e test against the runner's daemon.Testing
go build ./...,go vet ./...,gofmtclean;go test ./...green across all packages.tests/integration_testscoverage (execute, streaming SSE, files upload/download/list/delete incl. 413, sessions, bash, health).golang-checksjob).dhi.iobases (validate the refs/auth with aworkflow_dispatchdry run before the first release) and the privileged DinD bootstrap path, which needs a--privilegeddeployment to exercise.🤖 Generated with Claude Code