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 VarUsed ByDefault
DEV_PG_PORTdocker-compose.yml (postgres host port)5432
POSTGRES_PORTresources/.secrets.edn (app DB connection)5432
DEV_SHADOW_NREPL_PORTshadow-cljs.edn7002
DEV_SHADOW_HTTP_APP_PORTshadow-cljs.edn8081
DEV_SHADOW_HTTP_PORTFOLIO_PORTshadow-cljs.edn8082
PORTresources/system.edn (http-kit server)8080

3. Docker Compose Isolation — Three Modes

The :docker-compose key in :dev/options controls startup behaviour:

ValueBehaviour
:auto (default)TCP-probe postgres on the configured port. If reachable → skip docker compose. If not → start it.
trueAlways run docker compose up.
falseNever 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)

PortServiceConfig Source
5432PostgreSQL (→ 5432)docker-compose.yml
7002Shadow-CLJS nREPLshadow-cljs.edn
7888Clojure nREPL (Launchpad)bin/launchpad
8080HTTP Server (http-kit)resources/system.edn
8081Shadow dev HTTP (app)shadow-cljs.edn
8082Shadow 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

FlagDescription
--branch <name>Branch to use for this instance. Checks origin first; fetches locally if remote-only; creates from HEAD if new.
--worktreeCreate a git worktree for the instance. Requires --branch.
--docker-overrideGenerate docker-compose.override.yml for a fully isolated postgres container (unique name, volume, network).
-h, --helpShow 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)
OffsetService
+0PostgreSQL host port
+1nREPL
+2Shadow-CLJS nREPL
+3HTTP Server
+4Shadow HTTP (app)
+5Shadow 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