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:
⚡ Port conflict detected — some default ports are in use:
shadow-nrepl-port 7002 → 39669
nrepl-port 7888 → 40327
http-server-port 8080 → 45307
shadow-http-app-port 8081 → 43851
shadow-http-portfolio-port 8082 → 43019
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 launchpad script handles ports in three modes:
- Explicit ports — configured via
bb prepare-instance, stored in:dev/optionsindeps.local.edn. Used for stable multi-instance setups. - Auto-resolve — no explicit ports configured, but some defaults are occupied. Free alternatives are found automatically at startup.
- Defaults — all default ports are free. No overrides needed.
For explicit mode, the system writes port overrides into :dev/options inside deps.local.edn (gitignored). The launchpad script merges :dev/options from deps.edn (project defaults) and deps.local.edn (instance overrides, local wins):
bin/launchpad— merges:dev/optionsfrom both files, checks port availability, injects ports asDEV_*env vars and sets the nREPL portshadow-cljs.edn— readsDEV_*env vars via#shadow/envdocker-compose.yml— readsDEV_*env vars via${VAR:-default}resources/system.edn— readsPORTenv var via Aero#env
Default Ports (Single Instance)
| Port | Service | Config Source |
|---|---|---|
| 4334 | Datomic Transactor | docker-compose.yml |
| 5434 | 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 |
| 4335 | Datomic Health (ping) | docker-compose.yml |
| 11211 | Memcached | docker-compose.yml |
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 it locally if it exists remotely; creates it from HEAD if it does not exist anywhere. The branch name is also used as the port seed. |
--worktree | Create a git worktree for the instance. Requires --branch. The worktree is placed under the worktree base directory (see Worktree Directory below). Port config is written into the new worktree, not the current directory. |
--docker-override | Generate docker-compose.override.yml for fully isolated Docker containers (unique names, volumes, 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 containers
bb prepare-instance --worktree --docker-override --branch feat/my-feature
# With Docker container isolation (for worktrees that need separate DBs)
bb prepare-instance --docker-override 99
Worktree Directory
When --worktree is used, worktrees are created under a base directory that follows this convention by default:
<parent-of-repo>/<repo-name>-worktrees/<branch-name-with-slashes-as-dashes>/
For example, if your repo lives at ~/projects/myapp and you run:
bb prepare-instance --worktree --branch feat/prompt-crud
The worktree will be created at:
~/projects/myapp-worktrees/feat-prompt-crud/
To override the base directory, set :worktree-dir in deps.local.edn:
;; deps.local.edn
{:dev/options {:worktree-dir "/custom/path/to/worktrees"}}
bb list-instances
Shows all git worktrees and their configured ports.
bb list-instances
Port Allocation
Ports are computed as:
base = 10000 + (seed mod 10000)
| Offset | Service |
|---|---|
| +0 | PostgreSQL |
| +1 | nREPL |
| +2 | Shadow-CLJS nREPL |
| +3 | HTTP Server |
| +4 | Shadow HTTP (app) |
| +5 | Shadow HTTP (portfolio) |
| +6 | Memcached |
| +7 | Datomic Transactor |
| +8 | Datomic Health (ping) |
Example: seed 42 → base 10042 → ports 10042–10050.
Conflict Resolution
Before assigning ports, the system:
- Checks all other worktrees'
deps.local.ednfor reserved ports - TCP-probes each port to verify it's actually free
- If the preferred block is taken, scans upward in steps of 8
Configuration Details
Docker Compose (:docker-compose)
The :docker-compose option in :dev/options controls whether docker compose up runs at startup. Three modes:
| Value | Behavior |
|---|---|
:auto | (default) Probe PostgreSQL and Datomic Transactor on their configured ports. If both are reachable, skip docker compose up; otherwise start it. |
true | Always run docker compose up |
false | Never run docker compose up (useful if you manage services externally) |
;; deps.local.edn
{:dev/options {:docker-compose :auto}} ;; default — auto-detect
{:dev/options {:docker-compose false}} ;; never start docker
{:dev/options {:docker-compose true}} ;; always start docker
In :auto mode, probes are protocol-aware — not just TCP checks:
- PostgreSQL: Sends a PostgreSQL SSLRequest packet and verifies the response — reliably identifies PostgreSQL without requiring
pg_isreadyor any client tools - Datomic Transactor: Checks the HTTP health endpoint on the ping port (
/healthon port 4335 by default, mapped from container port 9999)
This catches cases where a port is occupied by a different service, which a raw TCP check would miss. If a port is occupied but fails the protocol check, the system warns you and starts docker-compose.
After docker-compose starts, services are verified again with the same protocol-aware probes (with retries and timeouts) to ensure they actually started correctly.
deps.local.edn
After running prepare-instance, your deps.local.edn will look like:
{:launchpad/options {:emacs true
:verbose true
: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}}
With --docker-override, Docker ports are also included in :dev/options:
{:launchpad/options {:emacs true
:verbose true
: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
:memcached-port 10048
:datomic-transactor-port 10049
:datomic-health-port 10050}}
Environment Variables
The launchpad script sets these env vars from :dev/options:
| Variable | Used By |
|---|---|
DEV_SHADOW_NREPL_PORT | shadow-cljs.edn |
DEV_SHADOW_HTTP_APP_PORT | shadow-cljs.edn |
DEV_SHADOW_HTTP_PORTFOLIO_PORT | shadow-cljs.edn |
DEV_PG_PORT | docker-compose.yml |
DEV_MEMCACHED_PORT | docker-compose.yml |
DEV_TRANSACTOR_PORT | docker-compose.yml |
DEV_TRANSACTOR_HEALTH_PORT | docker-compose.yml |
PORT | system.edn (http-kit) |
Docker Isolation (--docker-override)
When running multiple instances that need separate databases, use --docker-override. This generates a docker-compose.override.yml that Docker Compose automatically merges, giving each instance:
- Unique container names (e.g.,
dev-42-postgres) - Separate volumes (no data collision)
- Isolated Docker network
Without --docker-override, all instances share the same Docker containers (suitable when working on independent features that don't conflict in the DB).
Resetting to Defaults
To go back to standard single-instance ports:
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 port 10042
ports → {:postgres-port 10042, :nrepl-port 10043, ...}
│
└─► writes deps.local.edn {:dev/options {:nrepl-port 10043 ...}
:launchpad/options {:nrepl-port 10043}}
bb dev (bin/launchpad)
│
├─► merges :dev/options from deps.edn + deps.local.edn
│
├─► resolves nREPL port eagerly (before launchpad before-steps)
│ configured port free? → use it
│ occupied? → launchpad/free-port
│
├─► inject-instance-ports (one of three modes):
│ ├─ explicit :dev/options ports → use as-is
│ ├─ no explicit, defaults occupied → auto-resolve free ports
│ └─ all defaults free → no overrides
│ │
│ ├─► DEV_* env vars ──► docker-compose.yml (pg, memcached, transactor)
│ │ ──► shadow-cljs.edn (nrepl + dev-http ports)
│ └─► PORT env var ──► system.edn (http-kit server port)
│
├─► resolve-docker-compose (protocol-aware probes):
│ :auto → pg_isready + transactor /health → true/false
│ (warns if port occupied by wrong service)
│ true → true
│ false → false
│
├─► docker-compose-up (only if resolved to true)
├─► verify-docker-services (only if docker started):
│ pg_isready with retries (up to ~24s)
│ transactor /health with retries (up to ~75s)
│ ❌ warns if services fail to start
├─► start-css-watch
└─► launchpad starts nREPL on resolved port