local-stack
GitHub为每个 worktree 提供隔离的 LLM Gateway 本地栈,通过配置环境变量分配独立端口和容器名,解决多工作区共享机器时的端口冲突问题。
Trigger Scenarios
Install
npx skills add theopenco/llmgateway --skill local-stack -g -y
SKILL.md
Frontmatter
{
"name": "local-stack",
"description": "Run an isolated per-worktree LLM Gateway stack — own Postgres, Redis, storage Redis, and app ports — and tear it down safely. Use before running unit tests, starting dev servers, reseeding, or running docker compose in a worktree; when choosing STACK_SUFFIX, port slots, or .envrc; when a port is taken, a local server behaves like another worktree's code, or a relocated frontend fails CORS or login."
}
Local stack per worktree
Several worktrees share this machine, and only one stack can own the default
ports (:4001, :5432, :6379). Before assuming local edits are live, confirm which
tree serves a port: lsof -a -p <pid> -d cwd -Fn. Give each worktree its own
stack instead of sharing the defaults.
Every host-facing port and Docker name is env-var driven; the defaults reproduce the single shared stack.
Pick a slot
Pick a slot N (1, 2, 3 …) and offset every port:
| Component | Env var | Default | Slot N | Slot 1 |
|---|---|---|---|---|
| Postgres (host port) | POSTGRES_PORT |
5432 | 5432 + N × 100 | 5532 |
| Redis | REDIS_PORT |
6379 | 6379 + N × 1000 | 7379 |
| Storage Redis | STORAGE_REDIS_PORT |
6479 | 6479 + N × 1000 | 7479 |
| Gateway | GATEWAY_PORT |
4001 | 4001 + N × 100 | 4101 |
| Gateway metrics | METRICS_PORT |
9090 | 9090 + N × 100 | 9190 |
| API | API_PORT |
4002 | 4002 + N × 100 | 4102 |
| UI | UI_PORT |
3002 | 3002 + N × 100 | 3102 |
| Playground | PLAYGROUND_PORT |
3003 | 3003 + N × 100 | 3103 |
| Code | CODE_PORT |
3004 | 3004 + N × 100 | 3104 |
| Docs | DOCS_PORT |
3005 | 3005 + N × 100 | 3105 |
| Admin | ADMIN_PORT |
3006 | 3006 + N × 100 | 3106 |
| Airside | AIRSIDE_PORT |
3007 | 3007 + N × 100 | 3107 |
Redis uses ×1000 so slot 1's Redis does not land on another worktree's default storage-Redis port (6479).
Configure .envrc
Put the block in the worktree's gitignored .envrc (direnv; direnv allow
after editing). Exported vars reach docker compose, drizzle-kit, vitest,
and Node, and Node's --env-file never overrides an exported var.
# --- isolated stack: worktree "tel-aviv", slot 1 ---
export STACK_SUFFIX=-tel-aviv # compose project + container name suffix (include the separator)
export POSTGRES_PORT=5532
export REDIS_PORT=7379
export STORAGE_REDIS_PORT=7479
export DATABASE_URL=postgres://postgres:pw@localhost:5532/db
export TEST_DATABASE_URL=postgres://postgres:pw@localhost:5532/test
export GATEWAY_PORT=4101
export METRICS_PORT=9190
export API_PORT=4102
export UI_PORT=3102
export PLAYGROUND_PORT=3103
export CODE_PORT=3104
export DOCS_PORT=3105
export ADMIN_PORT=3106
export AIRSIDE_PORT=3107
# URLs the services hand to each other / render into pages
export API_URL=http://localhost:4102
export UI_URL=http://localhost:3102
export APP_URL=http://localhost:3102
export GATEWAY_URL=http://localhost:4101
export PLAYGROUND_URL=http://localhost:3103
export DOCS_URL=http://localhost:3105
export ADMIN_URL=http://localhost:3106
export AIRSIDE_URL=http://localhost:3107
export ORIGIN_URLS=http://localhost:3102,http://localhost:3103,http://localhost:3104,http://localhost:3105,http://localhost:3106,http://localhost:3107,http://localhost:4102
Agent shells do not run direnv. Prefix every command that depends on the block
with set -a && source .envrc && set +a.
Start
docker compose up -d # project llmgateway-tel-aviv, containers postgres-tel-aviv, …
pnpm wait-for-services
pnpm push-test
pnpm push-dev
pnpm seed
pnpm setup does all of this after a down -v; run it only with
STACK_SUFFIX set.
Turbo's strict env mode drops the undeclared port and DATABASE_URL vars, so
start dev servers with pnpm exec turbo run dev --env-mode=loose and confirm
the startup logs show your ports. To relocate one service, run it directly:
( cd apps/api && API_PORT=4102 API_URL=http://localhost:4102 UI_URL=http://localhost:3102 \
ORIGIN_URLS=http://localhost:3102,http://localhost:4102 \
node --enable-source-maps --env-file=../../.env dist/serve.js ) # build first: turbo run build --filter=api
dist/serve.js has no watch; rebuild and restart after changes. The worker runs
dist/index.js. Stop servers by PID (lsof -nP -iTCP:<port> -sTCP:LISTEN); a
pkill -f pattern also kills other worktrees' servers.
How the wiring works
- Docker:
docker-compose.ymlderives the project name (llmgateway${STACK_SUFFIX}) and eachcontainer_namefromSTACK_SUFFIX, sodown -vandpnpm setuptouch only your project. Address containers withdocker compose exec postgres …, notdocker exec postgres …. - Databases: apps read
DATABASE_URL; tests readTEST_DATABASE_URL, falling back toDATABASE_URL, thenpostgres://postgres:pw@localhost:5432/test. Set both:TEST_DATABASE_URLkeepspnpm test:unitoff the dev database, andpnpm push-testpushes to it. - Redis:
packages/cachereadsREDIS_HOST/REDIS_PORTandSTORAGE_REDIS_HOST/STORAGE_REDIS_PORT. AnySTORAGE_REDIS_*var enables the separate storage instance, so always setSTORAGE_REDIS_PORT. - Service ports: gateway and api fall back to
PORT; useGATEWAY_PORTandAPI_PORTinstead and leavePORTunset. A second gateway needsMETRICS_PORT. The Next.jsdevscripts readUI_PORT/PLAYGROUND_PORT/CODE_PORT/DOCS_PORT/ADMIN_PORT. - Auth + CORS: the API reads
ORIGIN_URLS(CORS and better-auth trusted origins) andUI_URL. Add every relocated frontend's origin toORIGIN_URLS. The session cookie is host-only forlocalhost, so login works across ports. - Frontends → backends: frontends read
API_URLandGATEWAY_URLserver-side inapps/*/src/lib/config-server.ts.
Tear down
Tear down a stack you started in this session once the work is finished:
set -a && source .envrc && set +a
echo "llmgateway${STACK_SUFFIX}" # must print your project, not plain "llmgateway"
docker compose down -v
Leave the default shared stack (no STACK_SUFFIX) and any stack you did not
start running: it is the user's development database, and -v destroys its
volumes.
Version History
- d41bf19 Current 2026-10-19 04:12


