Multi-Instance Development
Run multiple dev instances in parallel — useful for git worktrees, parallel coding agents, or working on multiple features simultaneously.
Quick Start
Just run bb dev — if any default port is already in use (e.g. by another project), the system automatically detects the conflict and picks free alternatives.
For stable, repeatable port assignments (worktrees, parallel agents), use prepare-instance:
# Set up custom ports for this worktree/checkout
bb prepare-instance 42
# Start as usual
bb dev
How It Works
The system has three layers working together:
1. bb prepare-instance — Port Configuration
Writes port overrides into deps.local.edn (gitignored) under :dev/options. Uses a numeric seed (ticket number, branch name hash, or explicit integer) to derive a stable, unique 6-port block per instance.
2. bin/launchpad — Port Injection at Startup
Before the REPL starts, reads :dev/options from the merged deps.edn + deps.local.edn and injects port overrides as environment variables that flow into every child process:
| Env Var | Used By | Default |
|---|---|---|
DEV_PG_PORT | docker-compose.yml (postgres host port) | 5432 |
POSTGRES_PORT | resources/.secrets.edn (app DB connection) | 5432 |
DEV_SHADOW_NREPL_PORT | shadow-cljs.edn | 7002 |
DEV_SHADOW_HTTP_APP_PORT | shadow-cljs.edn | 8081 |
DEV_SHADOW_HTTP_PORTFOLIO_PORT | shadow-cljs.edn | 8082 |
PORT | resources/system.edn (http-kit server) | 8080 |
3. Docker Compose Isolation — Three Modes
The :docker-compose key in :dev/options controls startup behaviour:
| Value | Behaviour |
|---|---|
:auto (default) | TCP-probe postgres on the configured port. If reachable → skip docker compose. If not → start it. |
true | Always run docker compose up. |
false | Never run docker compose up (manage services externally). |
When not using --docker-override, prepare-instance also writes :docker-project-name (the main repo name) into :dev/options. This ensures that if a worktree has to start docker compose itself, it uses the same compose project as the main checkout — and therefore shares the same postgres_data volume. No data divergence between instances.
Default Ports (Single Instance)
| Port | Service | Config Source |
|---|---|---|
| 5432 | PostgreSQL (→ 5432) | docker-compose.yml |
| 7002 | Shadow-CLJS nREPL | shadow-cljs.edn |
| 7888 | Clojure nREPL (Launchpad) | bin/launchpad |
| 8080 | HTTP Server (http-kit) | resources/system.edn |
| 8081 | Shadow dev HTTP (app) | shadow-cljs.edn |
| 8082 | Shadow dev HTTP (portfolio) | shadow-cljs.edn |
Usage
bb prepare-instance [options] [seed]
Computes a unique port block and writes to deps.local.edn.
The seed can be:
- A number:
42,1234 - A ticket ID:
SC-1234,PROJ-42-my-feature - A branch name:
my-feature(uses string hash for stable port assignment) - Omitted: uses the current git branch name
Options
| Flag | Description |
|---|---|
--branch <name> | Branch to use for this instance. Checks origin first; fetches locally if remote-only; creates from HEAD if new. |
--worktree | Create a git worktree for the instance. Requires --branch. |
--docker-override | Generate docker-compose.override.yml for a fully isolated postgres container (unique name, volume, network). |
-h, --help | Show help |
Examples
# Use current branch as seed
bb prepare-instance
# Explicit seed number
bb prepare-instance 42
# Ticket-style branch name
bb prepare-instance SC-1234-my-feature
# Create/fetch a branch and configure ports for it (in current directory)
bb prepare-instance --branch feat/my-feature
# Create a worktree + branch in one step (full agentic coding setup)
bb prepare-instance --worktree --branch feat/my-feature
# Worktree + branch + isolated Docker (separate postgres DB)
bb prepare-instance --worktree --docker-override --branch feat/my-feature
Worktree Directory
When --worktree is used, worktrees are created at:
<parent-of-repo>/<repo-name>-worktrees/<branch-name>/
Example: repo at ~/projects/shipclojure-pragma, branch feat/user-auth →
~/projects/shipclojure-pragma-worktrees/feat-user-auth/
Override the base directory via deps.local.edn:
{:dev/options {:worktree-dir "/custom/path/to/worktrees"}}
bb prepare-instance:worktree
Shorthand for bb prepare-instance --worktree. Prompts for a branch name if --branch is not provided.
bb list-instances
Shows all git worktrees and their configured ports.
bb list-instances
Port Allocation
base = 10000 + (seed mod 10000)
| Offset | Service |
|---|---|
| +0 | PostgreSQL host port |
| +1 | nREPL |
| +2 | Shadow-CLJS nREPL |
| +3 | HTTP Server |
| +4 | Shadow HTTP (app) |
| +5 | Shadow HTTP (portfolio) |
Example: seed 42 → base 10042 → ports 10042–10047.
Conflict resolution: before assigning ports the system checks all other worktrees' deps.local.edn and TCP-probes each port. If the preferred block is taken, it scans upward in steps of 6.
Configuration Details
deps.local.edn — Without --docker-override (shared postgres)
{:launchpad/options {:nrepl-port 10043}
:dev/options {:nrepl-port 10043
:shadow-nrepl-port 10044
:http-server-port 10045
:shadow-http-app-port 10046
:shadow-http-portfolio-port 10047
:docker-compose :auto
:docker-project-name "shipclojure-pragma"}}
:docker-project-name ensures that if this worktree starts docker compose, it joins the main checkout's compose project and shares the same postgres data.
deps.local.edn — With --docker-override (isolated postgres)
{:launchpad/options {:nrepl-port 10043}
:dev/options {:nrepl-port 10043
:shadow-nrepl-port 10044
:http-server-port 10045
:shadow-http-app-port 10046
:shadow-http-portfolio-port 10047
:postgres-port 10042}}
docker-compose.override.yml — With --docker-override
services:
postgres:
container_name: shipclojure-42-postgres
volumes:
- shipclojure_42_postgres_data:/var/lib/postgresql
volumes:
shipclojure_42_postgres_data:
networks:
default:
name: shipclojure_42_network
Note: No port bindings in the override file. The postgres host port is controlled by DEV_PG_PORT in the base docker-compose.yml:
ports:
- "${DEV_PG_PORT:-5432}:5432"
This avoids Docker Compose double-binding errors when merging base + override.
Running Tests in a Worktree
bb test automatically reads :postgres-port from deps.local.edn and forwards POSTGRES_PORT as an environment variable to the test process:
# In any worktree — connects to the right postgres automatically
bb test
# Pass kaocha flags as usual
bb test --focus my.namespace-test
This works because the app reads the DB port from #or [#env POSTGRES_PORT 5432] in resources/.secrets.edn, and bb test injects this env var when a port override is configured.
Resetting to Defaults
rm deps.local.edn docker-compose.override.yml
Or manually edit deps.local.edn and remove the :dev/options key.
Data Flow
bb prepare-instance 42
│
▼
seed 42 → base 10042
ports → {:postgres-port 10042, :nrepl-port 10043, ...}
│
└─► deps.local.edn
{:dev/options {:nrepl-port 10043
:docker-compose :auto
:docker-project-name "shipclojure-pragma"
...}
:launchpad/options {:nrepl-port 10043}}
bb dev (bin/launchpad)
│
├─► inject-instance-ports
│ reads :dev/options from deps.edn + deps.local.edn (merged)
│ sets env vars: DEV_PG_PORT, POSTGRES_PORT, DEV_SHADOW_*, PORT
│
├─► resolve-docker-compose
│ :auto → TCP-probe localhost:5432 (or configured port)
│ reachable → skip docker-compose
│ not reachable → start docker-compose
│
├─► docker-compose-up
│ passes DEV_PG_PORT + :docker-project-name (-p flag) so all
│ non-isolated worktrees share the same postgres_data volume
│
└─► launchpad starts nREPL on the configured port
bb test (in any worktree)
│
└─► reads :postgres-port from deps.local.edn
injects POSTGRES_PORT env var → clj process connects to right DB