Skip to navigation

Multi-Container Tasks

View as MarkdownOpen in Claude

Some tasks need more than one container: a database the agent queries, an API it calls, a simulated user, or a service that records what the agent did. Describe them with a Docker Compose file in the task’s environment/ folder. The harbor Environment Server starts each service as its own sandbox, joins them into one group for the episode, and stops the group when the episode ends.

Use Compose when the task needs a process with its own image, or one the agent must not control. A tool the agent calls can often run in the agent’s sandbox instead (see A task with its own MCP tool).

A Compose task needs a sandbox provider that connects the sandboxes of a group. On main, that is OpenSandbox. Other providers, Docker included, refuse to start a Compose group, so its episodes fail at seed. A compose_overlay that names Compose files makes every task of the run a group.

Layout

my-task/
├── task.toml
├── instruction.md
├── environment/
│ ├── Dockerfile
│ ├── docker-compose.yaml
│ └── seed.json
├── tests/test.sh
└── solution/solve.sh

Name the file environment/docker-compose.yaml, the name Harbor reads. Service main is the agent’s sandbox, and every other service is a sidecar:

services:
main:
image: ${MAIN_IMAGE_NAME}
depends_on:
- api
api:
image: ghcr.io/example/aw-api:1.0
volumes:
- ./seed.json:/app/seed.json:ro
expose:
- "8080"

${MAIN_IMAGE_NAME} is the task’s own image. Services reach each other by name, so the agent calls http://api:8080. A sidecar with a build: section is built once at prepare, like a build-mode Dockerfile (see Dockerfile modes and Harbor image builds).

Prepare (gym eval run or gym dataset validate) resolves the file, checks every service, and records each image’s configuration in compose-images.json at the dataset root. A task it cannot run is left out and listed in skipped.json with the reason, and gym eval run stops unless you pass --skip-unloadable. The server checks the document again at seed.

artifacts entries and [[verifier.collect]] hooks can name a service. A sidecar that writes a file for test.sh needs Sidecar inputs.

Run on OpenSandbox

The shipped OpenSandbox config does not enable networking between sandboxes. Add a deployment file and pass it with --config:

sandbox:
opensandbox:
networking:
enabled: true
runtime_requirements:
shm_size_metadata_key: <cluster label for shm size>
shared_storage:
host_path: /mnt/shared
volume_sources:
pgdata: datasets/pgdata
gym dataset validate my-dataset --sandbox opensandbox --config compose-deployment.yaml
SettingNeeded when
networking.enabledAlways.
networking.loopback_forwardingA service uses network_mode: service:<name>.
runtime_requirements.shm_size_metadata_keyA service sets shm_size. Name the label your cluster reads shared-memory size from, such as nemo.nvidia.com/shm.
runtime_requirements.capability_probesA service uses cap_add: one probe command per capability.
shared_storage.host_pathA service mounts a named volume or one mapped through volume_sources, or a sidecar binds a /logs path.
volume_sourcesA volume is external: true, or a bind source sits outside the task.
images.authA service image is private. Prepare reads its configuration with the same credentials. Compose images are pulled at their recorded digest, so key a Docker Hub image as myorg/, whether the file writes myorg/img or docker.io/myorg/img, and an image on another registry by its host prefix (see OpenSandbox Provider).

See OpenSandbox and Compose Services.

Template variables

Prepare resolves ${NAME} the way Harbor’s docker compose does, and the server resolves it again at seed. The first source that defines a name wins:

  1. Gym’s infrastructure variables, below.
  2. The task’s [environment].env. A whole value ${VAR} or ${VAR:-default} reads the process environment. An unset VAR with no default leaves the task out.
  3. The process environment of the command that runs prepare or the server.
  4. environment/.env, read as Compose reads it.
  5. At seed only, the server’s compose_variables.
  6. The default in the expression, such as ${VAR:-default}.

At prepare, a name no source sets becomes an empty string, with a warning. At seed it fails the episode before any sandbox starts.

VariableValue
MAIN_IMAGE_NAMEThe task’s resolved image.
CONTEXT_DIRThe task’s environment/ folder.
TEST_DIR/tests
COMPOSE_PROJECT_NAMEhb__<hash>, stable per task. Not the runtime project name.
PREBUILT_IMAGE_NAME[environment].docker_image, when declared.
CPUS, MEMORY[environment].cpus, and memory_mb as <n>M, when declared.
HOST_VERIFIER_LOGS_PATH, HOST_AGENT_LOGS_PATH, HOST_ARTIFACTS_PATHThe /logs paths. ENV_ spellings hold the same values.

A task’s Compose file can read any variable of the process environment, an API key included. Run third-party datasets from an environment that exports only what they need, and run prepare and the server with the same variables.

Values that belong to the deployment, such as a sidecar’s model endpoint, go in the server’s compose_variables. Each entry takes one source: a literal value, a server process variable env, or a model_server, whose sandbox-reachable URL becomes the value:

harbor_environment_server:
environment_servers:
harbor:
compose_variables:
SIM_USER_BASE_URL: {model_server: user_model}
SIM_USER_API_KEY: {env: USER_MODEL_API_KEY}
SIM_USER_MODEL: {value: gpt-5.5}

A service’s env_file entries are read at prepare and inlined, with no ${VAR} interpolation, into its environment, which wins over them. The file must sit under the task’s environment/ folder and must not be a symbolic link. A missing file leaves the task out unless the entry says required: false.

Service fields

Beyond image, command, entrypoint, environment, working_dir, user, depends_on, healthcheck, expose, labels, cap_add, shm_size and network_mode: service:<name>, these fields have a defined meaning. Any other service key, such as privileged or secrets, leaves the task out. So does a top-level key other than name, services, networks and volumes, such as version or an x- block. A depends_on entry with required: false or restart: true, or a user with a group, such as 1000:1000, fails when the group starts.

[environment.healthcheck] runs at seed as main’s user: when one is set, as a decimal string’s integer user ID; with no user: it runs as the image’s user. The agent keeps its own user.

FieldMeaning in a sandbox group
deployresources.limits.cpus and limits.memory size the sandbox, or reservations when limits leaves them out. A sidecar that reserves GPUs (count or device_ids) goes to the server’s gpu_sandbox_provider, when set. main follows the task’s [environment].gpus. count: all, limits.pids, replicas other than 1 and keys besides resources, replicas and restart_policy fail.
cpus, mem_limitSize the sandbox’s CPUs or memory when deploy.resources does not.
restartAccepted. Nothing restarts.
pull_policyalways, missing or never. if_not_present, daily, weekly and every_<n> count as missing. build fails unless prepare built the service.
platformMust match the server’s sandbox_provider_options.platform, linux/amd64 by default.
networksBecome name scopes on the group’s one network. internal: true, ipam, driver_opts and static addresses fail.
portsA container port alone. Fixed published ports, host_ip, non-TCP protocols and ranges fail.
extra_hostsHost-to-IP entries. host-gateway fails, and a non-root sidecar cannot take them.
post_startRun once the command starts. privileged hooks and pre_stop fail.
tmpfs, ulimits, sysctls, security_optNot supported on OpenSandbox, the only provider that starts a group, so the task is left out.

Volumes

VolumeHow the group runs it
Bind of a task file or folderUploaded before the service’s command runs. Keep the source under environment/, tests/, solution/ or steps/. :ro is not enforced, since the upload is a copy.
Named volume under top-level volumes:A folder on shared storage, mounted into each service that names it and removed when the group stops.
external: true volume, or a host path outside the taskMapped through volume_sources to a folder under the shared storage root, which outlives the group.
A /logs pathCreated in main. A sidecar that binds one shares it with main through shared storage.

Anonymous volumes, tmpfs volumes, bind propagation and volume drivers leave the task out. Keep data that test.sh must trust in tests/, which the server uploads after the agent’s turn.

Job-level Compose overlay

The compose_overlay setting of the harbor Environment Server attaches services, tools and a prompt template to every task of a run without editing the tasks, as Harbor’s extra_docker_compose, agents[].mcp_servers and prompt_template_path job settings do. Use it to run one backend, such as an agent memory service, beside every task. It goes in the server block (see Configure the server):

harbor_environment_server:
environment_servers:
harbor:
compose_overlay:
files: [/abs/path/memory-compose.yaml, /abs/path/memory-glue.yaml]
mcp_servers:
- {name: memory, transport: streamable-http, url: "http://memory:8888/mcp/"}
prompt_template: /abs/path/memory_prompt.md.j2
KeyWhat it does
filesMerged in order onto each task’s Compose document, as Compose merges -f files, !reset and !override included. An image-only task first becomes a one-service group, main.
mcp_serversMerged by name with the task’s [[environment.mcp_servers]]. The overlay wins on a name. The entries count as the task’s own, so a harness that does not start task tool servers is refused for every task unless they are granted (see A task with its own MCP tool).
prompt_templateA Jinja2 file using {{ instruction }}, rendered around the first turn’s instruction.
  • Write absolute paths. Relative paths inside an overlay file resolve against the task’s environment/ folder.
  • In an overlay file, compose_variables win over the task’s own sources, so a task cannot redirect an overlay sidecar.
  • A task service with an overlay service’s name merges with it, with a warning. Tag the overlay service !override to use its definition alone.
  • Prepare reads the block too, records the overlay’s images and checks each merged document. Put it in env.yaml or a --config file that both prepare and the server read, and prepare again after you change it. Image references must resolve at prepare, without compose_variables.
  • For a Dockerfile task without its own Compose file, attaching services keeps the image’s startup user and working directory: the entrypoint and image health check run as the Dockerfile’s final USER and WORKDIR, after its RUN lines, which can already write to named and external volumes. [agent].user and [environment].workdir apply to the agent’s commands. An explicit services.main.user or services.main.working_dir in an overlay file overrides the startup default.
  • Restart the servers after you edit the template. Prepare also compiles it, so install jinja2>=3.1 where you run gym (the ray extra includes it).