Documentation menu

Run your container

Submit any publicly pullable OCI image by digest — it runs GPU-adjacent to the cell, with no network in the control loop.

DockerGitHub

Container is the packaging for anything custom: your runtime, your preprocessing, your weights, one image. The cell runs it GPU-adjacent — no network hop in the control loop, which is why this is the lowest-latency packaging — and the run records the digest, so the artifact is pinned as tightly as the robot's firmware.

Prerequisites

The image reference

The image field of Policy.container() is an ordinary OCI reference; any registry works:

  • Docker Hub — docker.io/acme/skill@sha256:4e5b...
  • GHCR — ghcr.io/acme/skill@sha256:4e5b...

There are no pull-credential fields today: the image must be pullable without authentication — public, or served from a registry mirror you operate that allows anonymous pulls. Private-pull support does not exist yet, and this page won't pretend otherwise.

Keep the two permission surfaces distinct. Pull permission is registry-side and yours to configure — it governs who can fetch the image, and anonymous pull does not have to mean listed or discoverable. The running container's permissions are the cell's: it executes GPU-adjacent with no outbound network in the control loop. That isolation is the point of container mode — nothing between policy and actuator — and it is the opposite trade from endpoint mode, which keeps weights home by putting your network in the loop.

Pin the digest

Tags move; digests don't. Resolve the tag you tested to its digest and submit that:

docker buildx imagetools inspect ghcr.io/acme/skill:v4 \
  --format '{{println .Manifest.Digest}}'
sha256:4e5b1a7c9d0f2a3b5c6d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a90

A digest identifies the complete artifact — code, weights, preprocessing, runtime — which is more than a bare weights hash pins. Two images with identical checkpoints and different preprocessing are different policies, and their digests say so. If v4 is re-pushed tomorrow, a digest-pinned run still names exactly what ran.

What the cell provides

Resources are provisioned GPU-adjacent per cell, sized to what the image needs — CPU, GPU, memory. Your job is a server that boots when the container starts: the cell runtime feeds it the observations named by the declared observation_contract and expects actions in the declared action_space, at its declared rate. Between episodes the runtime signals an episode boundary, and your policy clears its per-episode state — physical scene reset is the cell's job (scripted or teleop-assisted), never your policy's. A formal wire-level spec — routes and schemas — is not yet published; this page describes the interface behaviorally because that is what exists to describe.

Connect

run a digest-pinned image
import roborama  # reads ROBORAMA_API_KEY from the environment

DIGEST = ("sha256:4e5b1a7c9d0f2a3b5c6d8e9f0a1b2c3d"
          "4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a90")

policy = roborama.Policy.container(
    image="ghcr.io/acme/skill@" + DIGEST,
    action_space="joint_delta_50hz",
    observation_contract="droid-3cam",
)

run = roborama.run(
    robot="g1-edu-pro@fw2.3",
    environment="kitchen-std@v1.2",
    policy=policy,
    task="load_dishwasher@v2",
    episodes="auto(ci=0.95, moe=0.03)",
    max_budget_usd=4000,
)
print(run.id)

The declared contracts do the same work as everywhere else: the submission is validated against g1-edu-pro@fw2.3 and the scene before any motor moves, and a mismatch is a free rejection, not a burned episode.

Validate

Three layers, in the order they can catch you:

  1. Locally, in mock mode

    ROBORAMA_MOCK=1 runs the full loop against packaged fixtures — no API key, no hardware, no charge. Wire it into the same CI job that builds the image.

    dry-run in mock mode
    import roborama
    
    # Mock mode: no API key, no hardware. The whole loop runs
    # offline against packaged fixtures — or set ROBORAMA_MOCK=1.
    client = roborama.Client(mock=True)
    
    quote = client.quote(
        robot="aloha2-pro",
        environment="cell-a",
        episodes=300,
    )
    print(quote)
    # {robot_hours: 10.0, env_hours: 10.0, usd: 860, queue_eta: "2h"}
    
    run = client.run(
        robot="aloha2-pro@fw1.1",
        environment="cell-a@v1.0",
        policy=roborama.Policy.checkpoint(
            hf="acme/act-so101-pick-v2",
            runtime="lerobot",
        ),
        task="pick_place@v1",
        episodes=300,
        max_budget_usd=1000,
    )
    print(run.result())
    # n=300  rate=0.843  ci95=(0.797, 0.881)
  2. At admission — free

    The declared action_space and observation_contract are validated server-side at submission. A mismatch returns contract_validation_failed (422); a rejected submission costs nothing.

  3. At cell startup

    The cell pulls the image and boots your server. A pull failure — a typo'd reference, a deleted tag, a registry demanding auth — surfaces here, not at admission, and there is no dedicated error code for it today: expect a run that fails at startup rather than a clean rejection. Digest-pin and keep the image anonymously pullable to make this layer boring.

Quote

A quote prices both meters before you commit. The fixture job — 600 episodes of g1-edu-pro in kitchen-std — resolves to 20.0 robot-hours × $148 plus 20.0 environment-hours × $39: $3,740, queue ETA about 6 hours. Expiry and priorities are covered in billing & budgets.

quote before you run
import roborama  # reads ROBORAMA_API_KEY from the environment

quote = roborama.quote(
    robot="g1-edu-pro",
    environment="kitchen-std",
    episodes=600,
)
print(quote)
# {robot_hours: 20.0, env_hours: 20.0, usd: 3740, queue_eta: "6h"}

Evaluate

Submission is an ordinary run(); the fields not set here — perturbation schedule, retention, priority — are that page's. Follow it live while episodes tick:

watch it live
import roborama  # reads ROBORAMA_API_KEY from the environment

run = roborama.runs.get("run_8842")

for event in run.watch():   # live: episode ticker + WebRTC stream URLs
    print(event.episode, event.status)

stream = roborama.streams.get("cell-g1-04")
print(stream)
# {webrtc: "wss://streams.roborama.com/cells/cell-g1-04/webrtc",
#  mjpeg: "https://streams.roborama.com/cells/cell-g1-04/mjpeg",
#  viewer_token: "vt_7Kq2mHentXw4"}

Inspect results

n=612  rate=0.874  ci95=(0.846, 0.898)  cost_usd=3812

The result also carries the failure clusters and both meters; every episode ships MCAP telemetry, video, and a replay spec, and run.report(format="pdf") renders the citable verification report. Retention windows and export formats belong to the data contract.

Compare versions

Same robot, same scene, same task, same declared contracts — different digest. That single-variable diff is what makes compare() meaningful: paired episodes, same initial conditions for both images, roughly half the episodes to separate them.

Troubleshoot

CodeSymptomFix
contract_validation_failed422 at submission; nothing ranthe declared action_space or observation_contract doesn't match the pinned robot and scene — fix the declaration or the pin
firmware_pin_unavailable422 at submissionthe requested model@firmware isn't installed on any cell — check robots.list() for available revisions
budget_exceededrun halts mid-suite at the cappartial results are kept with honest statistics; raise max_budget_usd and resubmit if the interval is too wide
queue_timeoutrun never scheduled within its priority windowresubmit, or use burst priority

Where next