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:

  1. Explicit ports — configured via bb prepare-instance, stored in :dev/options in deps.local.edn. Used for stable multi-instance setups.
  2. Auto-resolve — no explicit ports configured, but some defaults are occupied. Free alternatives are found automatically at startup.
  3. 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/options from both files, checks port availability, injects ports as DEV_* env vars and sets the nREPL port
  • shadow-cljs.edn — reads DEV_* env vars via #shadow/env
  • docker-compose.yml — reads DEV_* env vars via ${VAR:-default}
  • resources/system.edn — reads PORT env var via Aero #env

Default Ports (Single Instance)

PortServiceConfig Source
4334Datomic Transactordocker-compose.yml
5434PostgreSQL (→ 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
4335Datomic Health (ping)docker-compose.yml
11211Memcacheddocker-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

FlagDescription
--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.
--worktreeCreate 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-overrideGenerate docker-compose.override.yml for fully isolated Docker containers (unique names, volumes, 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 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)
OffsetService
+0PostgreSQL
+1nREPL
+2Shadow-CLJS nREPL
+3HTTP Server
+4Shadow HTTP (app)
+5Shadow HTTP (portfolio)
+6Memcached
+7Datomic Transactor
+8Datomic Health (ping)

Example: seed 42 → base 10042 → ports 10042–10050.

Conflict Resolution

Before assigning ports, the system:

  1. Checks all other worktrees' deps.local.edn for reserved ports
  2. TCP-probes each port to verify it's actually free
  3. 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:

ValueBehavior
:auto(default) Probe PostgreSQL and Datomic Transactor on their configured ports. If both are reachable, skip docker compose up; otherwise start it.
trueAlways run docker compose up
falseNever 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_isready or any client tools
  • Datomic Transactor: Checks the HTTP health endpoint on the ping port (/health on 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:

VariableUsed By
DEV_SHADOW_NREPL_PORTshadow-cljs.edn
DEV_SHADOW_HTTP_APP_PORTshadow-cljs.edn
DEV_SHADOW_HTTP_PORTFOLIO_PORTshadow-cljs.edn
DEV_PG_PORTdocker-compose.yml
DEV_MEMCACHED_PORTdocker-compose.yml
DEV_TRANSACTOR_PORTdocker-compose.yml
DEV_TRANSACTOR_HEALTH_PORTdocker-compose.yml
PORTsystem.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