# Deploying an Environment

This page covers packaging and sharing an environment once it runs locally: dependencies and the Dockerfile, `openenv build`, `openenv validate`, `openenv push` and connecting to the deployed environment. To create the environment first, follow [Your First Environment](../guides/first-environment).

| Command | Description |
|---------|-------------|
| `openenv build` | Build the Docker image locally |
| `openenv validate --level static --skip-build` | Validate the manifest contract in `openenv.yaml` |
| `openenv push` | Deploy to a Hugging Face Space |
| `openenv push --repo-id NAME` | Deploy to a specific Space |
| `openenv push --private` | Deploy as a private Space |
| `openenv push --registry ghcr.io/ORG` | Push the image to a Docker registry instead |

See the [CLI reference](../reference/cli) for every command and flag.

## Dependencies and Dockerfile

Declare Python dependencies in the environment's `pyproject.toml` and regenerate `uv.lock` with `uv lock`. Install anything else (system packages, binaries) in `server/Dockerfile`, before the step that removes temporary files.

The template's [`server/Dockerfile`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/cli/templates/openenv_env/server/Dockerfile) is a multi-stage build on `ghcr.io/huggingface/openenv-base` that installs your dependencies with `uv sync` and serves `server.app:app` on port 8000. Keep building from `openenv-base` so shared tooling stays available.

## Build the image

From the environment folder (needs Docker):

```bash
openenv build
```

`openenv build` works for standalone environments and for ones inside the OpenEnv repo, and sets the build arguments accordingly. Useful flags:

- `--tag/-t`: override the default tag, `openenv-<env_name>` without its `_env` suffix (`openenv-my` for `my_env`)
- `--build-arg KEY=VALUE`: pass Docker build arguments (repeatable)
- `--dockerfile/-f` / `--context/-c`: custom locations when experimenting
- `--no-cache`: force fresh dependency installs

## Validate

```bash
openenv validate --level static --skip-build
```

`openenv validate` reads the `validation:` contract in `openenv.yaml`, checks the normalized manifest against the selected severity policy and exits non-zero when a required check fails. The report's `levels_run` field records which levels ran. To validate a running server, use `openenv validate --url http://localhost:8000`.

## Push to Hugging Face Spaces

```bash
# Push the environment in the current folder to <your-username>/<env_name>
openenv push

# Push to a specific repo or namespace
openenv push --repo-id my-org/my-env

# Push to a Docker registry (web UI disabled by default)
openenv push --registry ghcr.io/my-org

# Override the base image and make the Space private
openenv push --base-image ghcr.io/huggingface/openenv-base:latest --private

# Set Space variables and secrets at push time
openenv push -e OPENSPIEL_GAME=tic_tac_toe --secret OPENAI_API_KEY=sk-...
```

Options:

- `DIRECTORY` (positional): path to the environment (defaults to the current directory)
- `--repo-id/-r`: Space name, as `name` or `namespace/name`
- `--registry`: push the image to Docker Hub, GHCR, etc.
- `--interface/--no-interface`: toggle the web UI (on by default for Spaces)
- `--base-image/-b`: override the Dockerfile `FROM`
- `--private`: make the Space private
- `--env-var/-e KEY=VALUE`: set a public Space variable (repeatable), overriding matching keys from `variables:` in `openenv.yaml`
- `--secret KEY=VALUE`: set a private Space secret (repeatable). The value is never logged
- `--hardware/-H`: Space hardware (for example `t4-medium`)
- `--count/-n`: deploy several Space instances, each with a numeric suffix
- `--create-pr`: open a Pull Request instead of pushing to the default branch
- `--exclude`: an ignore file with globs to leave out of the upload

The command logs you in if needed, validates `openenv.yaml`, adds the Hugging Face frontmatter to the README when needed and uploads the bundle. It deletes the remote files the environment no longer ships: files at the root of the Space or under a top-level directory of the bundle (e.g. `server/`) that are not uploaded again. Each one is listed before the upload. Files under other directories (e.g. `assets/` added by hand on the Space) and files matching the ignore patterns (the defaults and `--exclude`) are kept, so list in `--exclude` any root-level file you manage on the Space directly. Local build artifacts (`build/`, `*.egg-info`, `__pycache__`, dotfiles) are never uploaded.

Space variables and secrets are only applied on direct Hugging Face Space pushes. They are not available with `--registry` and cannot be staged through `--create-pr`.

### Declare public variables in `openenv.yaml`

Defaults that belong with the environment (game name, benchmark, max steps) go in a `variables:` block in `openenv.yaml`. `openenv push` applies them to the Space:

```yaml
variables:
  OPENSPIEL_GAME: catch
```

CLI `-e` overrides matching keys. Put secrets (API keys, tokens) only on the CLI with `--secret KEY=VALUE`, never in the yaml.

To fork or update someone else's environment, see [Contributing Environments](contributing-envs).

## Use the deployed environment

```python
from my_env import MyAction, MyEnv

# Pull the Space's image and run it locally (needs Docker)
client = MyEnv.from_env("my-org/my-env").sync()
# Or start a container from a local image
client = MyEnv.from_docker_image("openenv-my:latest").sync()
# Or connect to a server that is already running
client = MyEnv(base_url="http://localhost:8000").sync()

with client:
    result = client.reset()
    result = client.step(MyAction(message="Hello!"))
    state = client.state()
```

`from_docker_image()` and `from_env()` return a lazy bootstrap handle, not a connected client. Nothing starts until you resolve it: chain `.sync()` for a synchronous client, or `await` the handle from async code. Using the handle directly in a `with` block raises `TypeError: '_BootstrapResult' object does not support the context manager protocol`.

The async version:

```python
import asyncio

from my_env import MyAction, MyEnv

async def main():
    client = await MyEnv.from_docker_image("openenv-my:latest")
    async with client:
        result = await client.reset()
        result = await client.step(MyAction(message="Hello!"))

asyncio.run(main())
```

See [Async vs Sync](../guides/async-sync) for when to prefer each style and [Runtime Providers](../guides/runtime-providers) to run the image somewhere other than local Docker.

## Build in CI (OpenEnv repo only)

For an environment inside the OpenEnv repo, add it to the matrix in `.github/workflows/docker-build.yml` to build its image on every push to `main`:

```yaml
strategy:
  matrix:
    image:
      - name: echo-env
        dockerfile: envs/echo_env/server/Dockerfile
        context: envs/echo_env
      - name: my-env  # Add your environment here
        dockerfile: envs/my_env/server/Dockerfile
        context: envs/my_env
```

