File size: 14,784 Bytes
b4ac377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# Packaging & Deploying

**Part 4 of 5** in the OpenEnv Getting Started Series

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.

```{note}

**New to OpenEnv?** If you're just getting started, we recommend completing the [Getting Started tutorials](index) first. They provide a conceptual introduction to OpenEnv and reinforcement learning fundamentals. This guide is for developers ready to build production-quality environments.

```

## Quick Reference Card

Already familiar with OpenEnv? Here's the 8-step process at a glance:

| Step | Command / Action | Description |
|------|------------------|-------------|
| 1 | `openenv init my_env` | Scaffold new environment |
| 2 | Edit `models.py` | Define Action & Observation dataclasses |
| 3 | Edit `server/my_environment.py` | Implement `reset()` and `step()` methods |
| 4 | Edit `client.py` | Implement `_step_payload()`, `_parse_result()`, `_parse_state()` |
| 5 | `openenv serve` | Start local dev server for testing |
| 6 | `openenv validate` | Validate environment structure |
| 7 | `openenv push` | Deploy to Hugging Face Hub |
| 8 | Share the URL! | Others use via `MyEnv.from_hub("you/my-env")` |

### CLI Quick Reference

| Command | Description |
|---------|-------------|
| `openenv init NAME` | Scaffold new environment |
| `openenv serve` | Start local dev server |
| `openenv build` | Build Docker image |
| `openenv validate --verbose` | Validate environment structure |
| `openenv push` | Deploy to Hugging Face Hub |
| `openenv push --repo-id NAME` | Deploy to specific repo |
| `openenv push --private` | Deploy as private environment |
| `openenv push --registry ghcr.io/ORG` | Push to GitHub Container Registry |

```{tip}

For a hands-on tutorial that builds a complete environment step-by-step, see [Building & Sharing Environments](plot_03_building_environments) in the Getting Started series.

```

---

## Overview

A typical workflow looks like:

1. Scaffold a new environment with `openenv init`.
2. Customize your models, environment logic, and FastAPI server.
3. Implement a typed `EnvClient` (WebSocket-based for persistent sessions).
4. Configure dependencies and the Dockerfile once.
5. 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 [`uv`](https://github.com/astral-sh/uv) for 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`

```bash

# Run from anywhere – defaults to current directory

openenv init my_env



# Optionally choose an output directory

openenv init my_env --output-dir /Users/you/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 `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, environment, and client. For example, you will find `MyEnvironment`, `MyAction`, `MyObservation`, and `MyEnv` (client) in the `my_env` directory based on the name you provided. The environment uses the core `State` class from `openenv.core.env_server.types`.

### 2. Define Models

Edit `models.py` to describe your action and observation using Pydantic:

```python

# models.py

from pydantic import Field

from openenv.core.env_server.types import Action, Observation



class MyAction(Action):

    """Your custom action."""

    command: str = Field(..., description="Command to execute")

    parameters: dict = Field(default_factory=dict, description="Command parameters")



class MyObservation(Observation):

    """Your custom observation."""

    result: str = Field(..., description="Result of the action")

    success: bool = Field(..., description="Whether the action succeeded")

```

### 3. Implement Environment Logic

Customize `server/my_environment.py` by extending `Environment`:

```python

# server/my_environment.py

from uuid import uuid4

from openenv.core.env_server.interfaces import Environment

from openenv.core.env_server.types import State

from models import MyAction, MyObservation



class MyEnvironment(Environment):

    def __init__(self):

        self._state = State(episode_id=str(uuid4()), step_count=0)



    def reset(self) -> MyObservation:

        self._state = State(episode_id=str(uuid4()), step_count=0)

        return MyObservation(result="Ready", success=True, done=False, reward=0.0)



    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, done=False, reward=1.0)



    @property

    def state(self) -> State:

        return self._state

```

### 4. Create the FastAPI Server

`server/app.py` should expose the environment through `create_app`.

**Important:** You must pass a class or factory function (not an instance) to enable WebSocket-based concurrent sessions:

```python

# server/app.py

from openenv.core.env_server import create_app

from ..models import MyAction, MyObservation

from .my_environment import MyEnvironment



# Pass the class (factory) - each WebSocket session gets its own instance

app = create_app(MyEnvironment, MyAction, MyObservation, env_name="my_env")

```

For environments with constructor arguments, create a factory function:

```python

# server/app.py

import os

from openenv.core.env_server import create_app

from ..models import MyAction, MyObservation

from .my_environment import MyEnvironment



# Read config from environment variables

api_key = os.getenv("MY_API_KEY")

timeout = int(os.getenv("MY_TIMEOUT", "30"))



def create_my_environment():

    """Factory function that creates MyEnvironment with config."""

    return MyEnvironment(api_key=api_key, timeout=timeout)



# Pass the factory function

app = create_app(create_my_environment, MyAction, MyObservation, env_name="my_env")

```

### 5. Implement the Client

`client.py` extends `EnvClient` so users can interact with your server via WebSocket for persistent sessions:

```python

# client.py

from openenv.core.env_client import EnvClient

from openenv.core.client_types import StepResult

from .models import MyAction, MyObservation, MyState



class MyEnv(EnvClient[MyAction, MyObservation, MyState]):

    def _step_payload(self, action: MyAction) -> dict:

        return {"command": action.command, "parameters": action.parameters}



    def _parse_result(self, payload: dict) -> StepResult[MyObservation]:

        obs_data = payload.get("observation", {})

        obs = MyObservation(

            result=obs_data.get("result", ""),

            success=obs_data.get("success", False),

            done=payload.get("done", False),

            reward=payload.get("reward"),

        )

        return StepResult(

            observation=obs,

            reward=payload.get("reward"),

            done=payload.get("done", False),

        )



    def _parse_state(self, payload: dict) -> State:

        return State(

            episode_id=payload.get("episode_id"),

            step_count=payload.get("step_count", 0),

        )

```

The `EnvClient` maintains a persistent WebSocket connection to the server, enabling efficient multi-step interactions with lower latency compared to HTTP. Each client instance gets its own dedicated environment session on the server.

### 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:

<details>
<summary>Dockerfile</summary>

```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 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 is already in the pyproject.toml dependencies

# For standalone builds, openenv 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"]



```

</details>

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:

```bash

cd 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 default `openenv-<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:

```bash

# 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 to `cwd`)
- `--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 Dockerfile `FROM`
- `--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`:

```yaml

strategy:

  matrix:

    image:

      - name: echo-env

        dockerfile: envs/echo_env/server/Dockerfile

      - name: chat-env

        dockerfile: envs/chat_env/server/Dockerfile

      - name: coding-env

        dockerfile: envs/coding_env/server/Dockerfile

      - name: my-env  # Add your environment here

        dockerfile: envs/my_env/server/Dockerfile

```

### Use Your Environment

Here is a simple example of using your environment:

```python

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")



# Use context manager for automatic cleanup (recommended)

with client:

    # 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)



# Or manually manage the connection

try:

    client = MyEnv(base_url="http://localhost:8000")

    result = client.reset()

    result = client.step(MyAction(command="test", parameters={}))

finally:

    client.close()

```

## Nice work! You've now built and used your own OpenEnv environment.

Your next steps are to:

- [Try out the end-to-end tutorial](https://colab.research.google.com/github/meta-pytorch/OpenEnv/blob/main/examples/OpenEnv_Tutorial.ipynb)