Running in production¶
The quick start imports the library directly, which is the shortest path to a working sandbox. It is not how to run openblox where it matters.
Architecture¶
┌──────────────────────┐ Unix socket ┌────────────┐ Docker API ┌──────────────────┐
│ your application │ ───────────────► │ openbloxd │ ───────────► │ Docker + gVisor │
│ pkg/brokerclient │ (socket_group) │ (profiles) │ │ └► sandbox │
│ no Docker access │ │ holds the │ │ (runsc, no │
└──────────────────────┘ │ socket │ │ network) │
└────────────┘ └──────────────────┘
openbloxdowns the Docker socket. Your application talks to it over a Unix socket and never touches Docker.- Profiles in
openbloxd's config are the whole isolation policy: image, runtime, egress, user, resources, lifetime. A request names a profile; it cannot set any of these. - The container is the state. There is no database. Restarting
openbloxdloses nothing, and it reaps sandboxes on its own schedule.
Direct library mode holds root
Importing pkg/docker means your process talks to the Docker daemon, and access
to the Docker socket is equivalent to root on the host. If that process is
compromised — or tricked, for instance by a prompt injection reaching a code path
you did not intend — the host is. Use direct mode for development, or where the
process is as trusted as the host. Otherwise use openbloxd.
The full trust model is in THREAT_MODEL.md.
Supported environments¶
| Component | Supported | Tested |
|---|---|---|
| Host OS | Linux | Ubuntu (GitHub-hosted runners, every PR) |
| Architecture | linux/amd64, linux/arm64 |
both, natively, with the full gVisor integration suite on every PR |
| Docker Engine | a current release, with the API version negotiated | the release on GitHub's ubuntu-latest image; Docker 29.1 |
gVisor (runsc) |
a current release, registered as a Docker runtime named runsc; the default systrap platform needs no KVM |
the latest release at CI time; release-20260803.0 |
| Go (library users) | the version in go.mod (1.25) or newer |
1.25 |
| macOS, Windows, Docker Desktop | not supported — gVisor runs on Linux only | — |
Compatibility¶
| Pair | Rule |
|---|---|
openbloxd ↔ pkg/brokerclient |
Use the same release. The wire format is not versioned separately; fields are only added, and older clients ignore new ones, but only matching versions are tested together. |
| openblox ↔ sandbox image | Any image that satisfies the image contract. The reference image version X.Y.Z is built from the openblox tag vX.Y.Z, but any version of it works with any openblox release that has the same contract. |
| openblox ↔ gVisor | Any runsc Docker can start. openblox checks that a runtime named runsc is registered and fails with ErrRuntimeUnavailable if not — it never falls back to runc. |
| openblox ↔ Docker | Any Engine whose API the bundled client can negotiate with. |
Deploying openbloxd¶
Install it on the host, not in a container. Running it in a container with the Docker socket mounted puts back the privilege it exists to remove.
1. Install a verified release. Pick a version rather than latest, and verify it
before installing (details in
RELEASING.md):
VERSION=v0.6.1; ARCH=amd64
gh release download "$VERSION" -R blox-eng/openblox \
-p "openbloxd-linux-$ARCH" -p "openbloxd-linux-$ARCH.sha256" \
-p openbloxd.service -p openbloxd.example.yaml
sha256sum -c "openbloxd-linux-$ARCH.sha256"
gh attestation verify "openbloxd-linux-$ARCH" -R blox-eng/openblox \
--signer-workflow blox-eng/openblox/.github/workflows/publish-daemon.yml \
--source-ref "refs/tags/$VERSION"
sudo install -m 0755 "openbloxd-linux-$ARCH" /usr/local/bin/openbloxd
openbloxd --version # must print $VERSION, not "dev"
2. Create its user and the socket group.
sudo useradd --system --no-create-home --shell /usr/sbin/nologin openbloxd
sudo usermod -aG openbloxd <the user your application runs as>
openbloxd's primary group is openbloxd, which is the example config's
socket_group. See the security model
before choosing a different group.
3. Configure profiles. Start from openbloxd.example.yaml and install it as
/etc/openbloxd/config.yaml. Pin every image by digest, and size
max_sandboxes × memory_mb across all profiles to what the host can hold, with
headroom. Leave runtime: runsc and egress: none alone for untrusted code.
Unknown keys, negative bounds and root users are refused at start-up.
4. Install the unit and start it.
sudo install -m 0644 openbloxd.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now openbloxd
5. Use it from your application.
client, err := brokerclient.New("/run/openbloxd/openbloxd.sock")
if err != nil {
return err
}
defer client.Close()
sb, err := client.Create(ctx, "session-1", brokerclient.WithProfile("code-exec"))
brokerclient.Client implements the same sandbox.Backend as the Docker backend.
A containerized application mounts the directory /run/openbloxd, not the socket
file, so it survives a daemon restart.
Operating it¶
- Previews: serve them from an origin that shares no cookies or storage with your application, since the content is written by the sandbox. Keep TTLs short.
- Output:
Execkeeps up to 16 MiB of each stream (Result.Truncatedtells you when it cut). Treat everything a sandbox returns as untrusted input. - Timeouts: a timed-out command is killed with its process group. Code that
detaches with
setsidsurvives until the sandbox is reaped;Destroythe sandbox when work must certainly stop. - Lifetime:
openbloxdruns the reaper everyreap_interval. In direct library mode you must callBackend.Reapperiodically, or idle and max-age bounds are never enforced.
Upgrading¶
- Read the release's Breaking and Security entries in the changelog.
- Download and verify the new binary as in step 1 above.
sudo installit over the old one, thensudo systemctl restart openbloxd. Running sandboxes are not interrupted and are found again by name. If the new version rejects your config, it refuses to start and says why.- Upgrade
pkg/brokerclientin your application to the same version. - To move to a new sandbox image, update the profile's digest and restart. Existing sandboxes keep the image they were created with until they are destroyed or reaped; new ones use the new image.
To roll back, reinstall the previous binary and profile, and restart. Sandboxes carry no version state, so nothing needs migrating in either direction.
Troubleshooting¶
| Symptom | Cause and fix |
|---|---|
ErrRuntimeUnavailable / runtime "runsc" is not registered |
Register gVisor: sudo runsc install && sudo systemctl restart docker, then check with docker info --format '{{json .Runtimes}}'. |
A container starts but uname -r inside is not …-gvisor |
The runtime named runsc is not gVisor. Fix the path in /etc/docker/daemon.json. |
ErrImageUnavailable |
The image is not present and could not be pulled. For a private registry, set registry_auth in the profile (or docker.WithRegistryAuth). |
ErrInvalid: user … (root, not numeric, or a bare uid) |
Set user to an explicit, numeric, non-zero uid:gid, such as "1000:1000". |
HTTP 429, kind at_capacity |
The profile is at max_sandboxes. Retry after the reaper frees a slot, destroy unused sandboxes, or raise the cap if the host can hold it. |
HTTP 409, kind conflict |
The name already exists under another profile. Use a different name, or destroy the old sandbox. |
HTTP 409, kind stopped (ErrStopped) |
The sandbox exists but is not running, so it cannot serve exec, files or processes. Usually it hit memory_mb and was killed. The kill takes the whole sandbox, not the offending process, so anything it held is gone: create a fresh one rather than retrying. GET /sandboxes/{name} still resolves, so you can confirm the state. |
EACCES on the socket |
Your process is not in socket_group, or socket_group differs from the unit's group — see the security model. |
ENOENT on the socket after a daemon restart |
A container mounted the socket file rather than /run/openbloxd. Mount the directory. |
| Preview returns 502 | Nothing is listening on 127.0.0.1:<port> inside the sandbox, or the image has neither nc nor python3. |
Exec returns ErrTimeout |
The command exceeded its timeout, or the profile's max_timeout clamped a longer one. |
Result.Truncated is true |
A stream exceeded 16 MiB. Write the result to a file and use ReadFile. |
| Sandboxes accumulate (library mode) | Nothing is calling Reap. |
Docker logs Your kernel does not support swap limit capabilities |
cgroup v1 without swap accounting. The swap limit is dropped, so a sandbox may use swap on top of its memory limit. Prefer a cgroup v2 host. |
When not to use openblox¶
- You need tenants isolated from each other at the API: every
openbloxdcaller can reach every sandbox. Put your own authorisation in front of it. - You need several hosts, scheduling, or fair sharing: openblox is one host, one daemon.
- You need hardware-virtualisation isolation (a separate guest kernel per workload), or protection from side channels between co-resident workloads.
- You need sub-second cold starts, snapshots, or fork/resume.
- Your sandboxes need general network access.
unrestrictedegress exists, but then the network boundary is yours to build. - You cannot run Linux with gVisor, or cannot keep
runscpatched.