名称: 生成环境 描述: 创建、更新或审查项目的 gen-env 命令,用于在本地主机上运行多个隔离实例。处理实例身份、端口分配、数据隔离、浏览器状态分离和清理。
gen-env 技能
生成或审查一个 gen-env 命令,该命令允许在本地主机上同时运行项目的多个隔离实例(例如,多个工作树、功能分支或版本)。
问题
没有隔离的情况下,同一项目的多个实例会:
- 争夺硬编码端口(3000、5432、8080)
- 共享 Docker 卷 → 数据损坏
- 共享浏览器 cookie/localStorage → 身份验证混淆
- 容器名称不明确 → 无法区分哪个是哪个
- 存在灾难性清理风险 →
docker down -v会销毁一切
解决方案:实例身份
一切都源于一个工作空间名称:
名称 = "功能-x"
↓
┌─────────────────────────────────────────────────────┐
│ COMPOSE_PROJECT_NAME = 本地网络-功能-x │
│ DOCKER_NETWORK = 本地网络-功能-x │
│ VOLUME_PREFIX = 本地网络-功能-x │
│ CONTAINER_PREFIX = 本地网络-功能-x- │
│ TILT_HOST = 功能-x.localhost │
│ 端口 = 动态分配 │
│ URL = 基于主机 + 端口派生 │
└─────────────────────────────────────────────────────┘
隔离维度
1. 端口隔离
每个实例从临时端口范围(49152-65535)获取唯一端口。
2. 数据隔离
Docker Compose 项目名称控制卷命名:
- 实例 A:
本地网络-主分支_postgres_data - 实例 B:
本地网络-功能-x_postgres_data
无交叉污染。独立的数据库。
3. 网络隔离
每个实例有独立的 Docker 网络。容器通过服务名相互引用,不会冲突。
4. 浏览器状态隔离
关键点:localhost 上的不同端口仍然共享 cookie!
http://localhost:3000 ─┐
├─ 相同的 cookie、localStorage
http://localhost:3001 ─┘
解决方案:通过 *.localhost 进行子域隔离:
http://主分支.localhost:3000 ─ 独立的 cookie
http://功能-x.localhost:3001 ─ 独立的 cookie
Chrome/Edge 自动将 *.localhost 视为 127.0.0.1。无需修改 /etc/hosts。
5. 身份验证隔离
每个实例可以有自己的身份验证领域/受众,防止令牌混淆。
6. 资源命名
容器、卷、Tilt 资源、日志上的清晰前缀 → 确切知道您正在查看哪个实例。
实施清单
创建或审查 gen-env 时:
身份与命名:
- [ ] 需要
--name <工作空间>参数 - [ ] 验证名称(字母数字 + 短横线,DNS 最长 63 个字符)
- [ ] 根据名称生成
COMPOSE_PROJECT_NAME - [ ] 生成
DOCKER_NETWORK、VOLUME_PREFIX、CONTAINER_PREFIX - [ ] 为浏览器隔离生成
*_HOST(名称.localhost)
端口分配:
- [ ] 从临时范围(49152-65535)分配
- [ ] 分配前检查端口可用性
- [ ] 使用短超时(100 毫秒)以兼容 CI
- [ ] 优雅处理 IPv6 禁用环境
持久化:
- [ ] 锁文件存储名称 + 端口(
.gen-env.lock) - [ ] 锁文件存在且名称匹配时重用端口
- [ ]
--force重新生成所有内容 - [ ]
--clean删除生成的文件
输出:
- [ ] 生成
.localnet.env(或项目特定名称) - [ ] 带有生成时间戳的清晰标题
- [ ] 所有派生的 URL 使用正确的主机 + 端口
集成:
- [ ] 通过
.envrc将脚本添加到 PATH - [ ]
.envrc引用生成的 env 文件 - [ ] 与 Docker Compose 配合使用(
--env-file) - [ ] 与 Tilt 配合使用(Starlark 读取 env 文件)
生成的环境结构
# .localnet.env - 由 gen-env 生成
# 实例: 功能-x
# 生成时间: 2024-01-15T10:30:00Z
# === 实例身份 ===
WORKSPACE_NAME=功能-x
COMPOSE_NAME=本地网络-功能-x
COMPOSE_PROJECT_NAME=本地网络-功能-x
DOCKER_NETWORK=本地网络-功能-x
VOLUME_PREFIX=本地网络-功能-x
CONTAINER_PREFIX=本地网络-功能-x-
# === 主机(用于浏览器隔离) ===
APP_HOST=功能-x.localhost
TILT_HOST=功能-x.localhost
# === 分配的端口 ===
POSTGRES_PORT=51234
REDIS_PORT=51235
API_PORT=51236
WEB_PORT=51237
# ... 更多端口
# === 派生的 URL ===
DATABASE_URL=postgres://user:pass@localhost:51234/dev
WEB_URL=http://功能-x.localhost:51237
API_URL=http://功能-x.localhost:51236
direnv 集成
# .envrc
PATH_add bin # 或 scripts
dotenv_if_exists .localnet.env
参考实现(TypeScript/Bun)
有关完整实现,请参阅 @IMPLEMENTATION.md。
关键类型:
interface InstanceConfig {
name: string; // 工作空间身份
composeName: string; // Docker Compose 项目名称
dockerNetwork: string; // Docker 网络名称
volumePrefix: string; // Docker 卷前缀
containerPrefix: string; // 容器名称前缀
host: string; // 浏览器主机名(名称.localhost)
ports: Record<string, number>; // 分配的端口
urls: Record<string, string>; // 派生的 URL
}
interface LockfileData {
version: 1;
generatedAt: string;
instance: InstanceConfig;
}
清理模式
按实例进行精准清理:
# 仅清理功能-x(容器 + 卷 + 网络)
docker compose -p 本地网络-功能-x down -v
# 或通过 gen-env
gen-env --clean # 删除 .localnet.env 和 .gen-env.lock
# 列出所有本地网络实例
docker ps -a --filter "name=本地网络-" --format "table {{.Names}}\t{{.Status}}"
# 核选项(所有实例)- 危险
docker ps -a --filter "name=本地网络-" -q | xargs docker rm -f
docker volume ls --filter "name=本地网络-" -q | xargs docker volume rm
常见模式
模式 1:基于工作树的命名
# 从 git 工作树目录派生名称
WORKTREE_NAME=$(basename "$(git rev-parse --show-toplevel)")
gen-env --name "$WORKTREE_NAME"
模式 2:基于分支的命名
# 从分支派生名称
BRANCH=$(git branch --show-current | tr '/' '-')
gen-env --name "$BRANCH"
模式 3:显式命名
# 用户指定(建议为清晰起见)
gen-env --name bb-dev
gen-env --name testing-v2
审查清单
审查现有 gen-env 时:
- 它是否创建实例身份?(不仅仅是端口)
- 它是否设置 COMPOSE_PROJECT_NAME?(控制 Docker 命名)
- 它是否生成浏览器安全的主机?(
*.localhost) - URL 是否使用正确的主机派生?(不是硬编码的
localhost) - 清理是否精准?(可以删除一个实例而不影响其他实例)
- 锁文件是否存储名称?(用于跨运行的一致性)
- 它是否验证名称冲突?(如果锁文件有不同的名称则发出警告)
反模式
❌ URL 中硬编码 localhost
WEB_URL=http://localhost:${WEB_PORT} # 错误:共享 cookie
✅ 使用实例主机
WEB_URL=http://${APP_HOST}:${WEB_PORT} # 正确:隔离的 cookie
❌ 没有 COMPOSE_PROJECT_NAME
# 错误:使用目录名,可能冲突
docker compose up
✅ 显式项目名称
COMPOSE_PROJECT_NAME=本地网络-功能-x
docker compose up # 对所有资源使用项目名称
❌ 共享清理
docker compose down -v # 错误:哪个实例?
✅ 实例特定清理
docker compose -p 本地网络-功能-x down -v # 正确:明确
参考资料
- @IMPLEMENTATION.md - 完整的 TypeScript 实现
- @ADVANCED_PATTERNS.md - 复杂场景(单体仓库、CI、Tilt 集成)