Container Support¶
Run any environment inside a Docker container or isolated subprocess. The runner, inference backend, and training code stay on the host; only the environment (and its rewards, tool execution, and state) runs inside the container.
Why Containers?¶
- Safety: Agent actions execute in a sandbox and cannot affect the host
- Reproducibility: Pinned Docker images guarantee identical environment behavior
- Compatibility: Run Linux-only or dependency-heavy environments from any host
- Transparency: Users who don't use containers are completely unaffected
Quick Start: Process Runtime¶
The simplest way to isolate an environment — no Docker required:
from llenvs.container.config import ContainerConfig
from llenvs.core.config import EnvironmentConfig, EnvironmentFactory
config = EnvironmentConfig(
name="sudoku",
adapter="reasoning_gym",
size=100,
container=ContainerConfig(runtime="process"),
)
env = EnvironmentFactory.create(config)
# Use exactly like a local environment
state, info = env.reset(options={"task_index": 0})
print(state.observation.prompt)
The factory starts a subprocess running the environment server, connects a proxy client, and returns it. All reset(), step(), and compute_rewards() calls are forwarded over HTTP.
Docker Setup¶
Build an Image¶
Use the base Dockerfile:
Or create a minimal per-adapter image:
FROM python:3.11-slim
RUN pip install --no-cache-dir llenvs reasoning-gym
EXPOSE 8080
ENTRYPOINT ["python", "-m", "llenvs.container"]
Use in Config¶
config = EnvironmentConfig(
name="sudoku",
adapter="reasoning_gym",
size=100,
container=ContainerConfig(
runtime="docker",
image="llenvs-base:latest",
),
)
env = EnvironmentFactory.create(config)
YAML Configuration¶
environments:
- name: sudoku
adapter: reasoning_gym
size: 100
container:
runtime: docker
image: llenvs-reasoning-gym:latest
timeout: 60
env_vars:
REASONING_GYM_CACHE: /data/cache
volumes:
/host/data: /data
Process runtime (no Docker):
ContainerConfig Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
runtime |
str |
"docker" |
"docker" or "process" |
image |
str \| None |
None |
Docker image (required for docker runtime) |
port |
int \| None |
None |
Host port (None = auto-select) |
timeout |
float |
60.0 |
Max seconds to wait for server startup |
env_vars |
dict[str, str] |
{} |
Environment variables for the container |
volumes |
dict[str, str] |
{} |
Volume mounts (host_path -> container_path) |
docker_command |
str |
"docker" |
Path to docker CLI |
How It Works¶
Host Process Container / Subprocess
┌────────────────────────┐ ┌────────────────────────┐
│ TrajectoryRunner │ │ EnvironmentServer │
│ InferenceBackend │ HTTP │ ┌──────────────┐ │
│ ContainerEnvironment │◄────────────►│ │ Environment │ │
│ (proxy) │ JSON over │ │ (real impl) │ │
│ │ localhost │ └──────────────┘ │
│ Scorer │ │ Rewards, tools, │
│ DatasetProvider │ │ state — all here │
└────────────────────────┘ └────────────────────────┘
EnvironmentFactory.create()detects thecontainerfield- A runtime (Docker or subprocess) starts the environment server
- The server creates the real environment via the same factory (without container config)
- A
ContainerEnvironmentproxy is returned, forwarding all protocol calls over HTTP - Hidden state crosses as JSON; the client wraps it as
OpaqueHidden(supports attribute access) - Rewards are computed server-side;
reward_functionsreturns()on the proxy
Server Protocol¶
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/spec |
Environment spec |
GET |
/len |
Number of tasks |
GET |
/tools |
Available tool definitions |
GET |
/prompts |
Prompt components |
POST |
/reset |
Reset environment |
POST |
/step |
Take a step |
POST |
/compute_rewards |
Compute rewards for a transition |
Integrations¶
Scorer and DatasetProvider work through the proxy:
from llenvs.integrations.dataset_provider import DatasetProvider
from llenvs.integrations.scoring import Scorer
scorer = Scorer(env)
result = scorer.score(task_index=0, response="<answer>42</answer>")
provider = DatasetProvider(env)
item = provider[0]
Limitations¶
- SegmentedEnvironment: Don't wrap a
ContainerEnvironmentinSegmentedEnvironmenton the host — each segment would be an HTTP round-trip. If segmentation is needed, configure it inside the container. - info dicts must be JSON-serializable when using containers.
- reward_functions returns
()on the proxy. All reward computation happens server-side viastep()andcompute_rewards(). - One trajectory at a time for mutable-backend environments (WebShop, AgentGym, OpenEnv) — same constraint as local mode.
Building Custom Docker Images¶
For environments with heavy dependencies, create a focused image:
FROM python:3.11-slim
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Python packages
RUN pip install --no-cache-dir llenvs agentenv
EXPOSE 8080
ENTRYPOINT ["python", "-m", "llenvs.container"]
The entrypoint accepts --config (JSON) and --port arguments: