Building Your Own Environment with OpenEnv¶
This guide walks you through creating a custom environment using the OpenEnv framework and the openenv CLI.
The CLI handles scaffolding, builds, validation, and deployment so you can stay focused on environment logic.
Overview¶
A typical workflow looks like:
- Scaffold a new environment with
openenv init. - Customize your models, environment logic, and FastAPI server.
- Implement a typed
HTTPEnvClient. - Configure dependencies and the Dockerfile once.
- Use the CLI (
openenv build,openenv validate,openenv push) to package and share your work.
Note
These integrations are handled automatically by the openenv CLI when you run openenv init.
Prerequisites¶
- Python 3.11+ and
uvfor dependency locking - Docker Desktop / Docker Engine
- The OpenEnv library installed:
pip install https://github.com/meta-pytorch/OpenEnv.git
Step-by-Step Guide¶
Let's walk through the process of building a custom environment with OpenEnv.
1. Scaffold with openenv init¶
# Run from anywhere – defaults to current directory
openenv init my_env
# Optionally choose an output directory
openenv init my_env --output-dir /Users/you/src/envs
The command creates a fully-typed template with openenv.yaml, pyproject.toml, uv.lock, Docker assets, and stub implementations. If you're working inside this repo, move the generated folder under src/envs/.
Typical layout:
my_env/
├── __init__.py
├── README.md
├── client.py
├── models.py
├── openenv.yaml
├── pyproject.toml
├── uv.lock
└── server/
├── __init__.py
├── app.py
├── my_environment.py
├── requirements.txt
└── Dockerfile
Python classes are generated for the action, observation, and state, and a client is generated for the environment. For example, you will find MyEnvironment, MyAction, MyObservation, and MyState in the my_env directory based on the name of the environment you provided.
2. Define Models¶
Edit models.py to describe your action, observation, and state dataclasses:
# models.py
from dataclasses import dataclass
from core.env_server import Action, Observation, State
@dataclass
class MyAction(Action):
"""Your custom action."""
command: str
parameters: dict
@dataclass
class MyObservation(Observation):
"""Your custom observation."""
result: str
success: bool
@dataclass
class MyState(State):
"""Custom state fields."""
custom_field: int = 0
3. Implement Environment Logic¶
Customize server/my_environment.py by extending Environment:
# server/my_environment.py
import uuid
from core.env_server import Environment
from ..models import MyAction, MyObservation, MyState
class MyEnvironment(Environment):
def __init__(self):
super().__init__()
self._state = MyState()
def reset(self) -> MyObservation:
self._state = MyState(episode_id=str(uuid.uuid4()))
return MyObservation(result="Ready", success=True)
def step(self, action: MyAction) -> MyObservation:
# Implement your logic here
self._state.step_count += 1
result = self._execute_command(action.command)
return MyObservation(result=result, success=True)
@property
def state(self) -> MyState:
return self._state
4. Create the FastAPI Server¶
server/app.py should expose the environment through create_fastapi_app:
# server/app.py
from core.env_server import create_fastapi_app
from ..models import MyAction, MyObservation
from .my_environment import MyEnvironment
env = MyEnvironment()
app = create_fastapi_app(env, MyAction, MyObservation)
5. Implement the Client¶
client.py extends HTTPEnvClient so users can interact with your server over HTTP or Docker:
# client.py
from core.http_env_client import HTTPEnvClient
from core.types import StepResult
from .models import MyAction, MyObservation, MyState
class MyEnv(HTTPEnvClient[MyAction, MyObservation]):
def _step_payload(self, action: MyAction) -> dict:
return {"command": action.command, "parameters": action.parameters}
def _parse_result(self, payload: dict) -> StepResult[MyObservation]:
obs = MyObservation(**payload["observation"])
return StepResult(
observation=obs,
reward=payload.get("reward"),
done=payload.get("done", False),
)
def _parse_state(self, payload: dict) -> MyState:
return MyState(**payload)
6. Configure Dependencies & Dockerfile¶
The CLI template ships with pyproject.toml and server/Dockerfile. You should manage your python dependencies with uv or pip in the pyproject.toml file. Other dependencies should be installed in the Dockerfile.
Keep building from the openenv-base image so shared tooling stays available:
Dockerfile
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
# Multi-stage build using openenv-base
# This Dockerfile is flexible and works for both:
# - In-repo environments (with local src/core)
# - Standalone environments (with openenv-core from pip)
# The build script (openenv build) handles context detection and sets appropriate build args.
ARG BASE_IMAGE=openenv-base:latest
FROM ${BASE_IMAGE} AS builder
WORKDIR /app
# Build argument to control whether we're building standalone or in-repo
ARG BUILD_MODE=in-repo
ARG ENV_NAME=__ENV_NAME__
# Copy environment code (always at root of build context)
COPY . /app/env
# For in-repo builds, openenv-core is already in the pyproject.toml dependencies
# For standalone builds, openenv-core will be installed from pip via pyproject.toml
WORKDIR /app/env
# Install dependencies using uv sync
# If uv.lock exists, use it; otherwise resolve on the fly
RUN --mount=type=cache,target=/root/.cache/uv \
if [ -f uv.lock ]; then \
uv sync --frozen --no-install-project --no-editable; \
else \
uv sync --no-install-project --no-editable; \
fi
RUN --mount=type=cache,target=/root/.cache/uv \
if [ -f uv.lock ]; then \
uv sync --frozen --no-editable; \
else \
uv sync --no-editable; \
fi
# Final runtime stage
FROM ${BASE_IMAGE}
WORKDIR /app
# Copy the virtual environment from builder
COPY --from=builder /app/env/.venv /app/.venv
# Copy the environment code
COPY --from=builder /app/env /app/env
# Set PATH to use the virtual environment
ENV PATH="/app/.venv/bin:$PATH"
# Set PYTHONPATH so imports work correctly
ENV PYTHONPATH="/app/env:$PYTHONPATH"
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run the FastAPI server
# The module path is constructed to work with the /app/env structure
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
If you introduced extra dependencies in the Dockerfile, you should install them in the Dockerfile before removing temp files.
7. Build & Validate with the CLI¶
From the environment directory:
cd src/envs/my_env
openenv build # Builds Docker image (auto-detects context)
openenv validate --verbose
openenv build understands both standalone environments and in-repo ones. Useful flags:
--tag/-t: override the defaultopenenv-<env_name>tag--build-arg KEY=VALUE: pass multiple Docker build arguments--dockerfile/--context: custom locations when experimenting--no-cache: force fresh dependency installs
openenv validate checks for required files, ensures the Dockerfile/server entrypoints function, and lists supported deployment modes. The command exits non-zero if issues are found so you can wire it into CI.
8. Push & Share with openenv push¶
Once validation passes, the CLI can deploy directly to Hugging Face Spaces or any registry:
# Push to HF Spaces (auto enables web UI and prompts for login if needed)
openenv push
# Push to a specific repo or namespace
openenv push --repo-id my-org/my-env
# Push to Docker/ghcr (interface disabled by default)
openenv push --registry ghcr.io/my-org --tag my-env:latest
# Customize image base or visibility
openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest --private
Key options:
--directory: path to the environment (defaults tocwd)--repo-id: explicit Hugging Face space name--registry: push to Docker Hub, GHCR, etc.--interface/--no-interface: toggle the optional web UI--base-image: override the DockerfileFROM--private: mark the space as private
The command validates your openenv.yaml, injects Hugging Face frontmatter when needed, and uploads the prepared bundle.
9. Automate Builds (optional)¶
To trigger Docker builds on every push to main, add your environment to the matrix in .github/workflows/docker-build.yml:
strategy:
matrix:
image:
- name: echo-env
dockerfile: src/envs/echo_env/server/Dockerfile
- name: chat-env
dockerfile: src/envs/chat_env/server/Dockerfile
- name: coding-env
dockerfile: src/envs/coding_env/server/Dockerfile
- name: my-env # Add your environment here
dockerfile: src/envs/my_env/server/Dockerfile
Use Your Environment¶
For an end-to-end example of using your environment, see the Quick Start guide. Here is a simple example of using your environment:
from envs.my_env import MyAction, MyEnv
# Create environment from Docker image
client = MyEnv.from_docker_image("my-env:latest")
# Or, connect to the remote space on Hugging Face
client = MyEnv.from_hub("my-org/my-env")
# Or, connect to the local server
client = MyEnv(base_url="http://localhost:8000")
# Reset
result = client.reset()
print(result.observation.result) # "Ready"
# Execute actions
result = client.step(MyAction(command="test", parameters={}))
print(result.observation.result)
print(result.observation.success)
# Get state
state = client.state()
print(state.episode_id)
print(state.step_count)
# Cleanup
client.close()
Nice work! You've now built and used your own OpenEnv environment.¶
Your next steps are to: