Blog · 00 · 2026-10-10 · 10 min

Creating an environment on denv

Connect GitHub and a target. Describe the environment as a compose file. denv deploys it in phases, snapshots it, and lets you fork, replay, roll back, and hand it to any framework.

denv is the environment company: environments you can version, fork per rollout, and grade apart from the agent. Agents run in them, get evaluated in them, and models train against them. You bring the environment, built in whatever framework you already use. You connect GitHub and a place to run it. denv deploys it, keeps versions of it, snapshots it consistently, forks it for each rollout, and enforces the protections a lab will ask about. We never own your compute or your data.

This post is the whole journey, end to end. The framework posts that follow assume it.

What an environment is

A project has environments; alpha by default, forks with a parent version. Each environment has deployments, and every successful deployment or snapshot produces an immutable version: pinned digests plus a consistent snapshot.PROJECT · RL-ENVIRONMENTops-worldENVIRONMENTalpha · stage: alphaENVIRONMENTrollout-017 · forkDEPLOYMENTdeploy · 11 phasesVERSIONver_A · snapshotparent versionDEPLOYMENTfork · 4 phasesone project · many environments · every change is a deployment · every healthy result is a version
Fig. 1 The four nouns. A fork is an environment whose parent is a version.

An environment is a running system, not a container image. For an ops task it is a database with data, a queue with a backlog, an object store, and the box the agent gets a shell in. For a coding task it may be one container. Both are environments; the first is why denv exists.

denv's object model has four nouns:

Noun What it is Created by
Project A repo with a kind: rl-environment, agent, service, website, inference denv project create, or connecting a GitHub repo
Environment A running stack on one target. Every project gets alpha by default. A fork is an environment whose parent is a version deploy, fork
Deployment One change to an environment, run in phases, each with evidence deploy, snapshot, fork, rollback, promote, destroy
Version Pinned image digests plus a consistent snapshot of every volume, taken together every successful deploy or snapshot

An environment can have many deployments over its life. A version is immutable. A sandbox, the word frameworks use, is simply an environment created for one episode, usually by forking a version.

Step 1: describe the environment

The environment is a docker-compose.yaml in your repo, plus a small x-denv block. If you already have a compose file for your framework, it is likely usable as-is.

# docker-compose.yaml
x-denv:
  kind: rl-environment
  egress: deny                 # default; `allow` opts the stack onto the internet
  warm:                        # runs after health, before the snapshot
    - "psql -h db -U postgres -f /seed/orders.sql"
  agent:
    service: opsbox            # where the agent gets its shell; must not run as root
  verifier:
    image: ghcr.io/you/verifier:1.2
    command: ["/verify.sh"]
    collect: ["/home/ubuntu/answer.txt"]   # copied to a root-owned location before grading
  clock: frozen                # the stack sees the same wall clock every fork

services:
  db:
    image: postgres:16-alpine
    environment: { POSTGRES_PASSWORD: world }
    volumes: ["pg:/var/lib/postgresql/data"]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 2s
      retries: 30
  queue:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes"]
    volumes: ["redis:/data"]
    healthcheck: { test: ["CMD", "redis-cli", "ping"], interval: 2s, retries: 15 }
  opsbox:
    image: ubuntu:24.04
    user: "1000:1000"
    command: ["sleep", "infinity"]
    mem_limit: 2g
    pids_limit: 512
    environment: { HOME: /home/ubuntu, PGHOST: db, PGPASSWORD: world }
    volumes: ["home:/home/ubuntu"]
    depends_on: [db, queue]

volumes: { pg: {}, redis: {}, home: {} }

What denv reads from this:

  • Services, images, volumes, healthchecks, depends_on. Standard compose. Every declared volume becomes a dataset that is snapshotted and forked.
  • Caps. mem_limit, pids_limit, cpus. Services that omit them get defaults (4 GiB, 2048 pids, 2 CPUs). Caps are rendered into the stack on every target that supports them.
  • user. The agent service must not be root. If x-denv.agent.service runs as root, the deploy fails at resolve with a message saying so.
  • x-denv.egress. deny by default. The stack's network has no route out, and before every episode denv runs a probe from inside the network and records that it was blocked. allow is explicit.
  • x-denv.warm. Commands run after every service is healthy and before the snapshot, so the snapshot looks like a system that has been running, not one that just booted.
  • x-denv.verifier. The grader. It runs as a separate principal, after the agent's processes are stopped, on a fork, with the collect paths copied to a root-owned location first. Symlinks, FIFOs, devices and other special files in those paths are refused and the episode scores zero.
  • x-denv.clock. frozen pins the wall clock the stack observes to the snapshot time, so a fork replays identically. real is the default for non-RL kinds.

What denv refuses, with the reason in the deploy's resolve phase: network_mode: host, privileged, cap_add, bind mounts, build: without a prebuilt image on targets that cannot build, and undeclared volumes. The environment is a world, and the world should not reach the host.

If you have no compose file yet, denv init rl-environment writes the one above.

Step 2: connect GitHub

denv login
denv project create --kind rl-environment --source github:you/ops-world ops-world

Or use the console: New project → pick a kind → pick a repo. denv installs a GitHub App with read access to that repo and a status check. Every push to the default branch becomes a deploy of alpha; branches deploy to environments named after them when you ask. The status check links to the deployment's phase log.

Nothing is cloned onto denv's infrastructure beyond what the deploy needs to pin digests. Build output stays on your target.

Step 3: connect a target

Frameworks on the left call one sandbox API: create, exec, files, snapshot, fork, destroy. denv implements it and places the environment on the target the user connected: their own machines, provisioned clouds, or hosted sandbox providers.FRAMEWORKSHarborInspectOpenEnvverifiersHUDKarotteSkyRL · verl · TRL · prime-rlDENV · SANDBOX APIcreateexecfilessnapshot · whole systemfork · whole systemdestroyversions · verdict logTARGETSyour machines · denvdHetzner · AWS (provisioned)E2BDaytonaModalPrime Sandboxes
Fig. 2 denv sits between the framework and the target. Frameworks only see the sandbox API.

A target is where environments run. You connect at least one per team; you can connect several and choose per environment.

Your own machines. Any Linux box with Docker. Install the sidecar:

curl -fsSL https://get.denv.ai | sh -s -- --token $(denv host join-token)

denvd is one static binary. It opens one outbound WebSocket to the control plane and never listens on a port. The join token is single-use and becomes a per-host secret. With a ZFS pool (--zfs-pool denv) snapshots are atomic across every volume and a fork takes about a second; without one it reports storage: dir and copies, which is correct and slower. denv host list shows what each host reported.

Provisioned machines. denv cloud connect hetzner --token … or denv cloud connect aws --profile …. When a deploy has no host with capacity, the provision phase creates one with cloud-init that installs denvd and joins. Machines are yours, in your account, deletable by you.

Hosted sandbox providers. denv cloud connect e2b --api-key …, likewise daytona, modal, prime. No sidecar; the control plane drives the provider's API with your key. These targets run single-container environments only, and snapshot and fork are as fast as the provider makes them. The console shows the capability row for the target you picked, so you are never promised a feature the target cannot deliver:

Capability Your machines (denvd) E2B Daytona Modal Prime Sandboxes
Multi-service stack yes no no no compose, in progress
Consistent snapshot across volumes ZFS pause + snapshot snapshot to image filesystem + memory snapshot snapshot
Fork time ~1 s on ZFS seconds tens of seconds seconds seconds
Egress deny, verified yes provider network config limited block_network yes
Caps enforced yes provider tier provider tier per sandbox per sandbox
GPU planned no partial yes yes

Step 4: deploy

denv deploy --project ops-world --env alpha --wait
A deploy runs eleven phases in order: resolve, place, provision, join, prepare, start, health, warm, snapshot, ingress, verdict. When a phase fails the rest are skipped and the verdict reverts the environment to its previous version.resolveplaceprovisionjoinpreparestarthealthwarmsnapshotingressverdicthealth fails → skip → verdictrevert to previous healthy version · status rolled_backDEPLOY · EVERY PHASE RECORDS EVIDENCE
Fig. 3 A deploy. Each phase records evidence; a failure skips to verdict and reverts.

A deploy runs these phases, each recorded with evidence, visible in the console and the CLI as they happen:

resolve     parse compose, apply rules, pin every image to a digest → Version
place       choose a host (snapshot locality, capacity) or a provider
provision   create a machine if the pool is empty
join        wait for the sidecar to connect
prepare     datasets created, images pulled by digest
start       stack up in its own network
health      every service passes its healthcheck
warm        x-denv.warm commands
snapshot    writers frozen, databases checkpointed, every dataset captured together
ingress     names and TLS for published services
verdict     version marked healthy; alpha points at it

If any phase fails, the remaining phases are skipped, the verdict records why, and the environment is restored to its previous healthy version. The deployment's status is rolled_back, not failed, and the agent never sees a half-deployed world.

The snapshot is the heart of it. Databases are checkpointed, writers are paused, every volume is captured in the same instant, then writers resume and health is re-checked. The frozen window is measured and recorded in the phase evidence. A version is that snapshot plus the pinned digests.

Step 5: use it

Fork for a rollout. A fork is a new environment created from a version. On ZFS it is a clone, about a second, with the data present:

denv fork --name rollout-017 alpha
Version A is forked into an episode. Midway, the live state is snapshotted as version B, and eight branches fork from B in about a second each, data included. The original episode continues untouched.VERSION A · HEALTHY SNAPSHOTver_Afork ≈ 1 sstep 1step 2snapshot → ver_Bstep 3 …8 branches from ver_Bgraded ✓discardedonly declared volumes persist across snapshot and fork · process memory does not
Fig. 4 Snapshot mid-episode, then fork. Branches share state at the snapshot and nothing after.

Snapshot mid-episode. Capture the live state of a running environment as a new version, so you can branch from the middle of an episode:

denv snapshot rollout-017
denv fork --name rollout-017-b rollout-017

Exec and files.

denv exec --service opsbox rollout-017 -- psql -c "select count(*) from orders"
denv files put rollout-017 --service opsbox ./fixtures /home/ubuntu/fixtures
denv files get rollout-017 --service opsbox /home/ubuntu/answer.txt ./out/

Roll back. denv rollback alpha ver_… restores a previous version's snapshot and re-checks health.

denv verify: the agent runs in fork A as an unprivileged user with egress denied. Its processes are stopped, declared submission paths are copied to a root-owned location, special files are refused, fork B is made, the verifier image grades in fork B as a separate principal, and the reward and evidence land in the record.DENV VERIFY · THE GRADER NEVER SHARES A PROCESS TREE WITH THE AGENTagent runsagent runs fork A · uid 1000 · caps · egress ✗stopstop every agent process killedcollectcollect declared paths → root-owned copyrefuserefuse symlink · fifo · device → score 0forkfork fork B from Agradegrade verifier image · separate principalrecordrecord reward + evidence in the deploymentfork A is left as the agent left it, for inspection
Fig. 5 denv verify. Grading happens on a fork, as a separate principal, after the agent is stopped.

Verify. denv verify rollout-017 stops the agent's processes, collects the declared paths to a root-owned location, refuses special files, forks the environment, and runs the verifier image in the fork with those paths mounted read-only. The reward and the verifier's output land in the deployment's evidence. The agent's own fork is untouched, so you can inspect what it did.

Promote. denv promote ops-world alpha staging runs the gates: fresh fork, dataset consistency, N identical replays, invariants (nothing deleted, disabled, or silenced), a soak through the next scheduled trigger, then points staging at the version. The gate record is the environment's data sheet.

Export. denv export --harbor ops-world@ver_… ./out writes a Harbor task directory per task, with the pinned image, so the environment runs on any Harbor backend. denv export --openenv emits an OpenEnv provider image.

Step 6: hand it to a framework

Frameworks never see phases. They see a sandbox API:

Operation Endpoint
Sandbox for one episode POST /v1/sandboxes with {from_version} or {image, cmd, env, ports, volumes, egress}
Run a command POST /v1/envs/{id}/exec
Files in and out PUT /v1/envs/{id}/files?service=&path= (tar body), GET …/files
Snapshot live state POST /v1/envs/{id}/snapshot
Fork POST /v1/envs/{id}/fork
Destroy DELETE /v1/envs/{id}

The Python package wraps it: pip install "denv[all]", then -e denv.harbor:DenvEnvironment for Harbor, sandbox="denv" for Inspect, provider=DenvProvider() for OpenEnv. Each framework post shows exactly how.

For sandboxes created from an image, only the paths listed in volumes (default /work) persist across snapshot and fork. The image's writable layer is not data. Frameworks that write elsewhere lose it on fork, by design, and the adapters set the right paths for each framework.

What you get by default

For the rl-environment kind, every environment, on every target that supports it:

  1. The agent is never root. Enforced at resolve.
  2. The grader is a separate principal, after the agent stops, on a fork. The agent cannot tamper with grading because grading does not happen where the agent is.
  3. Special files are refused. Symlinks, FIFOs, devices in collected paths score zero.
  4. Memory, pid and cpu caps on every service. Rendered into the stack, verified by the sidecar.
  5. Egress denied and proven. A probe runs from inside the network before every episode; the result is in the evidence.

Everything the deployment did is in the phase log. That log is what you show a lab.

Status

Ships today. Projects with kinds (rl-environment, agent); alpha by default; denvd on your machines with ZFS or dir storage; Hetzner provisioning; deploy with the eleven phases above and rollback on failure; snapshot as a deployment kind; fork; rollback; exec; files in and out; POST /v1/sandboxes; caps, unprivileged agent user, egress deny with a verified probe; x-denv.egress, x-denv.warm, x-denv.agent (non-root enforced at resolve) and x-denv.verifier with collect; denv verify as a deployment kind (stop → collect → snapshot → clone → grade → verdict) with symlinks, FIFOs, devices and sockets refused for a score of 0, the grader run on a fork as root in its own image or inside a service, and the agent left frozen unless --resume; denv files put|get; Harbor, Inspect and OpenEnv adapters passing conformance, including the verify attack cases (2026-10-11).

Committed by this post. x-denv.clock: frozen; promote gates and the data sheet; GitHub App and push-to-deploy; denv login and the console; E2B, Daytona, Modal and Prime Sandboxes targets with the capability table above; AWS provisioning; GPU device passthrough; denv export --harbor and --openenv; denv import harbor|karotte; host-side image builds; ingress names and TLS; MCP server. Each lands with a dated note here.