#docker-compose #port #isolation #dev-environment #cli

app isolenv

Isolated runtime manager for parallel development environments

1 unstable release

Uses new Rust 2024

new 0.1.0 May 7, 2026

#359 in Command line utilities

MIT/Apache

115KB
3K SLoC

isolenv

A development environment manager that doesn't conflict when run in parallel.

isolenv hands each task its own independent ports / env file / DB / Redis / Docker Compose project. When humans, AI coding agents, and CI all hammer the same project at the same time, ports, databases, and .env.local files stop fighting over each other.

isolenv up issue-123    # 1 task = 1 environment
isolenv up issue-456    # gets different ports, different DB, different compose project — automatically

日本語版: README.ja.md


Problems this solves

  • Two dev servers fight over port 3000
  • A stray .env.local quietly redirects your app to staging / production DB
  • Migrations from parallel jobs collide and seed data gets cross-contaminated
  • Someone runs docker compose down -v and wipes a teammate's working volume
  • AI coding agents working in multiple git worktrees clash on ports
  • Frontend and backend both want PORT=3000 when run on the host

Installation

From crates.io

cargo install isolenv

Pre-built binaries

Download the archive for your platform from the GitHub Releases page and place the isolenv binary somewhere on your PATH.

Platform Asset
macOS (Apple Silicon) isolenv-aarch64-apple-darwin.tar.gz
macOS (Intel) isolenv-x86_64-apple-darwin.tar.gz
Linux (x86_64) isolenv-x86_64-unknown-linux-gnu.tar.gz
Linux (aarch64) isolenv-aarch64-unknown-linux-gnu.tar.gz
Windows (x86_64) isolenv-x86_64-pc-windows-msvc.zip
# macOS / Linux example (replace the target triple)
curl -L https://github.com/2ero20ne/isolenv/releases/latest/download/isolenv-aarch64-apple-darwin.tar.gz | tar xz
sudo mv isolenv /usr/local/bin/

mise

mise can install via either backend:

# Build from crates.io
mise use -g "cargo:isolenv"

# Or download a pre-built binary from GitHub Releases via ubi
mise use -g "ubi:2ero20ne/isolenv"

Nix

Without a flake in this repo, the simplest path is cargo install inside a Rust toolchain shell:

nix-shell -p cargo rustc --run 'cargo install isolenv'

Build from source

git clone https://github.com/2ero20ne/isolenv
cd isolenv
cargo install --path .

Try it in 30 seconds

mkdir my-app && cd my-app
isolenv init                  # generate isolenv.yaml + docker-compose.isolenv.yml
isolenv up issue-1            # allocate ports, write env, bring up compose
isolenv status                # see what's running
isolenv exec issue-1 dev      # run a configured command with the task env injected
isolenv clean issue-1 --yes   # tear it all down (volumes included)

.isolenv/issue-1/.env will hold things like FRONTEND_PORT, DATABASE_URL, and project commands run via isolenv exec issue-1 ... automatically point at the ports and DB allocated to that task.


Typical workflows

1. Run the same repo as two parallel tasks

isolenv up issue-123     # → FRONTEND_PORT=3100, DB_PORT=33300, compose project = ..._issue_123
isolenv up issue-456     # → FRONTEND_PORT=3101, DB_PORT=33301, compose project = ..._issue_456

isolenv exec issue-123 dev:frontend   # serves on PORT=3100
isolenv exec issue-456 dev:frontend   # serves on PORT=3101 from another terminal — no conflict

Ports and compose projects are assigned independently, so the two environments fully coexist. Three or more works the same way.

2. Wire up AI coding agents

isolenv init --agents adds AGENTS.md and CLAUDE.md to your project with rules like "don't run docker compose directly", "don't create .env.local", and "read ports from the env file." Claude Code, Codex, Cursor, and most other agents pick these up at the project root, which keeps them from making isolation-breaking mistakes.

isolenv init --agents              # for new projects
isolenv print-agents >> AGENTS.md  # to append into an existing AGENTS.md

Existing files are never overwritten. Merging is left to you.

3. Use it without Docker (CI, lightweight tools)

Just remove compose: from isolenv.yaml. Port allocation, env injection, and state management still work; docker compose is never invoked.

version: 1
project: my-tool
env:
  output: .isolenv/{task}/.env
services:
  app:
    ports:
      APP_PORT:
        range: [3100, 3999]
isolenv up issue-1            # no docker
isolenv exec issue-1 ...      # runs straight on the host

4. Run commands inside a container

Add commands.<name>.container: <service> and that command runs via docker compose exec <service> ... instead of on the host. Handy for running migrations against a containerized backend.

commands:
  migrate:
    container: backend
    run: pnpm prisma migrate dev
isolenv exec issue-1 migrate    # runs inside the container

Configuration: isolenv.yaml

version: 1
project: my-project
compose:
  file: docker-compose.isolenv.yml    # omit to skip Docker entirely
env:
  output: .isolenv/{task}/.env
  inherit: [PATH, HOME, SHELL, LANG]  # only inherit this allowlist from the host env
services:
  app:
    ports:
      APP_PORT:
        range: [3100, 3999]           # picks a free port from this range
    env:
      APP_URL: "http://localhost:${APP_PORT}"
  mysql:
    ports:
      DB_PORT:
        range: [33300, 33999]
    env:
      DATABASE_URL: "mysql://root:password@127.0.0.1:${DB_PORT}/app"
    wait:
      tcp: "127.0.0.1:${DB_PORT}"     # block `up` until this port accepts TCP
      timeoutSeconds: 30
commands:
  test: pnpm test
  dev:server:
    run: pnpm dev:server
    env:
      PORT: "${APP_PORT}"             # PORT is injected only for this command
  migrate:
    container: app                    # runs via `docker compose exec app ...`
    run: pnpm prisma migrate dev
  dev:                                # run multiple commands concurrently
    parallel: [dev:server, dev:worker]
guards:
  forbidEnvFiles:
    - .env.local                      # block exec/run if these exist
    - .env.production
  forbidHostPatterns:
    - rds.amazonaws.com               # block up if any generated env contains these
    - production

Service readiness (wait)

Each service can declare a probe that runs after up and blocks until the service is reachable:

wait:
  tcp: "127.0.0.1:${DB_PORT}"        # ready when TCP accepts
  # OR
  http: "http://localhost:${PORT}/health"  # ready when any HTTP/1.x response comes back
  timeoutSeconds: 30                  # default 60

tcp and http are mutually exclusive. https:// is not supported. Probes poll every 500 ms.

Variable interpolation

${VAR} references in service env: values resolve transitively — they can refer to ports or to other env vars. Undefined or circular references fail loudly.

What ends up in the env file

.isolenv/<task>/.env always contains:

  • ISOLENV_TASK / ISOLENV_PROJECT / ISOLENV_COMPOSE_PROJECT
  • All allocated port vars (FRONTEND_PORT=3100, DB_PORT=33300, …)
  • All services.*.env entries (interpolated)

PORT is never set globally (so frontend and backend can both run on the host without colliding). Inject it per-command via commands.<name>.env: { PORT: "${FRONTEND_PORT}" } instead.


CLI reference

Command What it does
isolenv init [--agents] Generate skeleton config files. --agents also writes AGENTS.md / CLAUDE.md. Existing files are never overwritten.
isolenv up <task> [--no-start] Allocate ports, write env, start compose. --no-start skips docker compose up.
isolenv down <task> Stop compose. Volumes are preserved.
isolenv clean <task> --yes Stop + remove volumes + remove .isolenv/<task>/.
isolenv clean --all --yes Clean every task in this project.
isolenv exec <task> <name> [-- extra args] Run a command from commands with the task env.
isolenv run <task> -- <command...> Run an arbitrary command with the task env.
isolenv list-commands List commands and where each runs (host / container / parallel).
isolenv print-env <task> [--export] Print the env file to stdout (--export for shell eval).
isolenv print-agents Print the canonical AGENTS.md / CLAUDE.md snippet to stdout.
isolenv status Show all tasks and their state.
isolenv doctor Diagnose config / Docker / guards.

Safety nets

isolenv stops commands when it detects a setup that would break isolation:

  • guards.forbidEnvFiles: refuses exec / run when files like .env.local exist (prevents frameworks from silently loading production credentials).
  • guards.forbidHostPatterns: refuses up if any generated env value contains a forbidden substring (e.g. rds.amazonaws.com, production).
  • Compose is always invoked with COMPOSE_PROJECT_NAME set, and clean refuses to run docker compose down -v without an explicit project name. There is no path that wipes volumes without naming a task.
  • Port allocation goes through an OS-level file lock, so two isolenv up invocations never assign the same port to different tasks.
  • If up fails partway, ports do not leak — isolenv clean <task> --yes always recovers.

isolenv doctor additionally warns if env.inherit allowlists obviously risky vars like DATABASE_URL, AWS_*, or KUBECONFIG.


Layout

.
├── isolenv.yaml                  # config
├── docker-compose.isolenv.yml    # compose file isolenv invokes
├── AGENTS.md / CLAUDE.md         # generated by `init --agents` (optional)
└── .isolenv/
    ├── lock.json                 # global port reservations (don't edit by hand)
    └── <task-id>/
        ├── .env                  # task env
        └── state.json            # task state

AGENTS.md and CLAUDE.md are generated with identical content by init --agents (Claude Code reads CLAUDE.md; Codex / Cursor / etc. read AGENTS.md). Delete whichever one your project doesn't use.


License

MIT OR Apache-2.0

Dependencies

~3.5–5.5MB
~99K SLoC