gen-env Skill
Generate or review a gen-env command that enables running multiple isolated instances of a project on localhost simultaneously (e.g., multiple worktrees, feature branches, or versions).
The Problem
Without isolation, multiple instances of the same project:
-
Fight for hardcoded ports (3000, 5432, 8080)
-
Share Docker volumes → data corruption
-
Share browser cookies/localStorage → auth confusion
-
Have ambiguous container names → can't tell which is which
-
Risk catastrophic cleanup → docker down -v nukes everything
The Solution: Instance Identity
Everything flows from a workspace name:
name = "feature-x" ↓ ┌─────────────────────────────────────────────────────┐ │ COMPOSE_PROJECT_NAME = localnet-feature-x │ │ DOCKER_NETWORK = localnet-feature-x │ │ VOLUME_PREFIX = localnet-feature-x │ │ CONTAINER_PREFIX = localnet-feature-x- │ │ TILT_HOST = feature-x.localhost │ │ Ports = dynamically allocated │ │ URLs = derived from host + ports │ └─────────────────────────────────────────────────────┘
Isolation Dimensions
- Port Isolation
Each instance gets unique ports from ephemeral range (49152-65535).
- Data Isolation
Docker Compose project name controls volume naming:
-
Instance A: localnet-main_postgres_data
-
Instance B: localnet-feature-x_postgres_data
No cross-contamination. Independent databases.
- Network Isolation
Separate Docker networks per instance. Containers reference each other by service name without collision.
- Browser State Isolation
Critical: Different ports on localhost still share cookies!
http://localhost:3000 ─┐ ├─ SAME cookies, localStorage http://localhost:3001 ─┘
Solution: subdomain isolation via *.localhost :
http://main.localhost:3000 ─ separate cookies http://feature-x.localhost:3001 ─ separate cookies
Chrome/Edge treat *.localhost as 127.0.0.1 automatically. No /etc/hosts needed.
- Auth Isolation
Each instance can have its own auth realm/audience, preventing token confusion.
- Resource Naming
Clear prefixes on containers, volumes, Tilt resources, logs → know exactly which instance you're looking at.
Implementation Checklist
When creating or reviewing gen-env:
Identity & Naming:
-
Requires --name <workspace> argument
-
Validates name (alphanumeric + dashes, max 63 chars for DNS)
-
Generates COMPOSE_PROJECT_NAME from name
-
Generates DOCKER_NETWORK , VOLUME_PREFIX , CONTAINER_PREFIX
-
Generates *_HOST for browser isolation (name.localhost )
Port Allocation:
-
Allocates from ephemeral range (49152-65535)
-
Checks port availability before assignment
-
Uses short timeout (100ms) for CI compatibility
-
Handles IPv6-disabled environments gracefully
Persistence:
-
Lockfile stores name + ports (.gen-env.lock )
-
Reuses ports when lockfile exists and name matches
-
--force regenerates all
-
--clean removes generated files
Output:
-
Generates .localnet.env (or project-specific name)
-
Clear header with generation timestamp
-
All derived URLs use correct host + port
Integration:
-
Script added to PATH via .envrc
-
Generated env sourced by .envrc
-
Works with Docker Compose (--env-file )
-
Works with Tilt (Starlark reads env file)
Generated Environment Structure
.localnet.env - generated by gen-env
Instance: feature-x
Generated: 2024-01-15T10:30:00Z
=== Instance Identity ===
WORKSPACE_NAME=feature-x COMPOSE_NAME=localnet-feature-x COMPOSE_PROJECT_NAME=localnet-feature-x DOCKER_NETWORK=localnet-feature-x VOLUME_PREFIX=localnet-feature-x CONTAINER_PREFIX=localnet-feature-x-
=== Host (for browser isolation) ===
APP_HOST=feature-x.localhost TILT_HOST=feature-x.localhost
=== Allocated Ports ===
POSTGRES_PORT=51234 REDIS_PORT=51235 API_PORT=51236 WEB_PORT=51237
... more ports
=== Derived URLs ===
DATABASE_URL=postgres://user:pass@localhost:51234/dev WEB_URL=http://feature-x.localhost:51237 API_URL=http://feature-x.localhost:51236
direnv Integration
.envrc
PATH_add bin # or scripts
dotenv_if_exists .localnet.env
Reference Implementation (TypeScript/Bun)
See @IMPLEMENTATION.md for full implementation.
Key types:
interface InstanceConfig { name: string; // Workspace identity composeName: string; // Docker Compose project name dockerNetwork: string; // Docker network name volumePrefix: string; // Docker volume prefix containerPrefix: string; // Container name prefix host: string; // Browser hostname (name.localhost) ports: Record<string, number>; // Allocated ports urls: Record<string, string>; // Derived URLs }
interface LockfileData { version: 1; generatedAt: string; instance: InstanceConfig; }
Cleanup Patterns
Surgical cleanup per instance:
Clean only feature-x (containers + volumes + networks)
docker compose -p localnet-feature-x down -v
Or via gen-env
gen-env --clean # removes .localnet.env and .gen-env.lock
List all localnet instances
docker ps -a --filter "name=localnet-" --format "table {{.Names}}\t{{.Status}}"
Nuclear option (all instances) - DANGEROUS
docker ps -a --filter "name=localnet-" -q | xargs docker rm -f docker volume ls --filter "name=localnet-" -q | xargs docker volume rm
Common Patterns
Pattern 1: Worktree-Based Naming
Derive name from git worktree directory
WORKTREE_NAME=$(basename "$(git rev-parse --show-toplevel)") gen-env --name "$WORKTREE_NAME"
Pattern 2: Branch-Based Naming
Derive name from branch
BRANCH=$(git branch --show-current | tr '/' '-') gen-env --name "$BRANCH"
Pattern 3: Explicit Naming
User specifies (recommended for clarity)
gen-env --name bb-dev gen-env --name testing-v2
Review Checklist
When reviewing an existing gen-env:
-
Does it create instance identity? (not just ports)
-
Does it set COMPOSE_PROJECT_NAME? (controls Docker naming)
-
Does it generate a browser-safe host? (*.localhost )
-
Are URLs derived with correct host? (not hardcoded localhost )
-
Is cleanup surgical? (can remove one instance without affecting others)
-
Does the lockfile store the name? (for consistency across runs)
-
Does it validate name conflicts? (warn if lockfile has different name)
Anti-Patterns
❌ Hardcoded localhost in URLs
WEB_URL=http://localhost:${WEB_PORT} # BAD: shares cookies
✅ Use instance host
WEB_URL=http://${APP_HOST}:${WEB_PORT} # GOOD: isolated cookies
❌ No COMPOSE_PROJECT_NAME
BAD: uses directory name, may conflict
docker compose up
✅ Explicit project name
COMPOSE_PROJECT_NAME=localnet-feature-x docker compose up # Uses project name for all resources
❌ Shared cleanup
docker compose down -v # BAD: which instance?
✅ Instance-specific cleanup
docker compose -p localnet-feature-x down -v # GOOD: explicit
References
-
@IMPLEMENTATION.md - Full TypeScript implementation
-
@ADVANCED_PATTERNS.md - Complex scenarios (monorepos, CI, Tilt integration)