---
title: "Run tickets in parallel without collisions"
description: "A worktree isolates code, not databases or ports. Use .fredrin/setup.sh to give each ticket worktree its own database and port. Rails, Node and Django."
url: "https://www.fredrin.com/docs/parallel-worktrees"
updated: "2026-10-19"
---

# Run tickets in parallel without collisions

A worktree gives each ticket its own files, but not its own database or port. A short setup script you commit to the repository gives it both.

## The problem

A [worktree](https://www.fredrin.com/docs/board/worktrees) isolates code. It does not isolate anything outside its folder. Every ticket's checkout still reads the same `.env` values and `config/database.yml` defaults, so every ticket still points at the same development database, the same test database and the same port.

Run three tickets at once and they collide:

- **Ports.** Two dev servers both try port 3000, and the second one dies with `EADDRINUSE`.
- **Schema.** One ticket's migration changes the shared database under another ticket's tests.
- **Test data.** Two test suites clean and reseed one test database at the same moment, and runs fail at random or deadlock.

The fix is to give every worktree its own database names and its own port. You write that once, as a script, and commit it.

## The fix: `.fredrin/setup.sh`

Commit a shell script at `.fredrin/setup.sh`. When Fredrin creates a fresh worktree for a ticket, it runs the script there before the Worker starts, so the Worker lands in a checkout that already has its own databases.

| | |
|---|---|
| **Where it runs** | On a [remote agent](https://www.fredrin.com/docs/remote-agent): any machine running `fredrin-agent`. Not for Workers the desktop app starts on its own machine (see [below](#on-the-desktop-app-s-own-machine)). |
| **When** | Once, right after the ticket's worktree is created. A reused worktree (a resume, or another run of the same ticket) skips it, except one whose `node_modules` has since been removed, which runs it again when the script offers `--dependencies`. |
| **How** | `sh .fredrin/setup.sh` from the worktree root. The file does not need its executable bit. |
| **Arguments** | `--dependencies` if that word appears anywhere in the script's text, otherwise none. A script that never mentions it is never handed a flag it does not understand. |
| **Environment** | The machine's own environment, plus `FREDRIN_HOOK=setup` and a `PATH` widened to the usual CLI directories. No Fredrin credentials: no API token and no API address. |
| **Time limit** | 10 minutes, or 15 when run with `--dependencies`. Then the script and every process it started are killed. |
| **On failure** | Advisory. A non-zero exit or a timeout is written to the job log as a `[setup]` line, and the Worker starts anyway, so it can read the line and repair. |

After the script, if the worktree still has no `.env` but the project commits a `.env.example`, Fredrin copies the example to `.env`. It never overwrites a `.env` the script wrote. To turn the script off on one machine, set `FREDRIN_AGENT_NO_SETUP=1` in that agent's environment.

### On the desktop app's own machine

Today the desktop app does not run `.fredrin/setup.sh` for Workers it starts on its own machine. Those worktrees keep sharing whatever your configuration points at until something prepares them. Two ways to close that gap:

- **Run it yourself** in the worktree: `sh .fredrin/setup.sh`.
- **Tell the Worker to run it.** The [instructions snippet](#tell-your-workers) below makes a missing `.env.local` the first thing a Worker fixes.

One part is already handled: when the desktop app starts a ticket's dev server for you, it picks a free port and exports it as `PORT`.

## Rails and Postgres

This script names everything after the worktree folder, plus a checksum of its full path so two folders with the same name never share a database. It writes the names to `.env.local`, then creates and loads both databases.

```sh
#!/bin/sh
# .fredrin/setup.sh: give this ticket worktree its own databases and port.
set -eu

app=myapp                       # prefix for this app's database names
pg=postgres://localhost:5432    # your Postgres server; add user:password@ if it needs them

# A name unique to this worktree: the folder name, plus a checksum of the
# full path so two folders with the same name never share a database.
folder=$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9\n' '_' | cut -c1-30)
sum=$(printf '%s' "$PWD" | cksum | cut -d ' ' -f 1)
slug="${folder}_$((sum % 100000))"
port=$((3100 + sum % 800))      # stable per worktree, 3100 to 3899, never 3000

dev_url="$pg/${app}_dev_$slug"
test_url="$pg/${app}_test_$slug"

cat > .env.local <<ENV
DATABASE_URL=$dev_url
TEST_DATABASE_URL=$test_url
PORT=$port
ENV
# dotenv-rails does not load .env.local in the test environment, so tests read this one.
printf 'TEST_DATABASE_URL=%s\n' "$test_url" > .env.test.local

bundle check >/dev/null 2>&1 || bundle install
DATABASE_URL="$dev_url" bin/rails db:prepare
RAILS_ENV=test TEST_DATABASE_URL="$test_url" bin/rails db:prepare
```

`db:prepare` creates a database that does not exist yet and loads the schema, so the Postgres role needs permission to create databases. The derivation uses only POSIX shell, `tr`, `cut` and `cksum`, so it gives the same answer under `dash` (`/bin/sh` on Debian and Ubuntu) and `bash`. Two worktrees can occasionally land on the same port; edit `PORT` in that worktree's `.env.local` if they do.

### Node and Prisma

Same derivation, then Prisma's migrations against both databases. Prisma's CLI does not read `.env.local`, so the script passes each URL inline.

```sh
#!/bin/sh
set -eu
app=myapp
pg=postgres://localhost:5432
folder=$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9\n' '_' | cut -c1-30)
sum=$(printf '%s' "$PWD" | cksum | cut -d ' ' -f 1)
slug="${folder}_$((sum % 100000))"
dev_url="$pg/${app}_dev_$slug"
test_url="$pg/${app}_test_$slug"

printf 'DATABASE_URL=%s\nTEST_DATABASE_URL=%s\nPORT=%s\n' \
  "$dev_url" "$test_url" "$((3100 + sum % 800))" > .env.local

[ -d node_modules ] || npm ci
DATABASE_URL="$dev_url" npx prisma migrate deploy
DATABASE_URL="$test_url" npx prisma migrate deploy
```

`prisma migrate deploy` creates the database if it does not exist. Next.js reads `.env.local` on its own; for plain Node use `node --env-file=.env.local`, and have your test setup use `TEST_DATABASE_URL`.

### Django

Django's test runner already names its test database after the main one (`test_` plus the name), so a per-worktree main database gives you a per-worktree test database for free. Django does not create the main one, so the script does.

```sh
#!/bin/sh
set -eu
app=myapp
pg=postgres://localhost:5432
folder=$(basename "$PWD" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9\n' '_' | cut -c1-30)
sum=$(printf '%s' "$PWD" | cksum | cut -d ' ' -f 1)
db="${app}_dev_${folder}_$((sum % 100000))"

printf 'DATABASE_URL=%s\nPORT=%s\n' "$pg/$db" "$((3100 + sum % 800))" > .env.local

psql "$pg/postgres" -tAc "SELECT 1 FROM pg_database WHERE datname = '$db'" | grep -q 1 \
  || psql "$pg/postgres" -c "CREATE DATABASE $db"
DATABASE_URL="$pg/$db" python manage.py migrate
```

## Point the app at it

The script only writes names. The app has to read them.

**Rails.** Read the URLs in `config/database.yml`, and add `dotenv-rails` to the `:development` and `:test` groups of your Gemfile so `.env.local` and `.env.test.local` are loaded:

```yaml
development:
  <<: *default
  database: myapp_development
  url: <%= ENV["DATABASE_URL"] %>

test:
  <<: *default
  database: myapp_test
  url: <%= ENV["TEST_DATABASE_URL"] %>
```

Rails merges a `DATABASE_URL` it finds in the process environment into whichever environment is running, tests included. So never `export DATABASE_URL` in a shell you run tests from; let dotenv load it. That is why the test database's name lives in its own `.env.test.local`.

**Node and Prisma.** Nothing to change if `schema.prisma` (or `prisma.config.ts`) already reads `env("DATABASE_URL")`.

**Django.** Build `DATABASES` from the URL, for example with `dj-database-url`, after loading `.env.local` with `python-dotenv`:

```python
from dotenv import load_dotenv
import dj_database_url

load_dotenv(BASE_DIR / ".env.local")
DATABASES = {"default": dj_database_url.config(default="postgres://localhost:5432/myapp")}
```

**Ports.** Most servers decide their port before they read any `.env` file, so pass it on the command line, read straight from the file:

```sh
bin/rails server -p "$(sed -n 's/^PORT=//p' .env.local)"
npx next dev -p "$(sed -n 's/^PORT=//p' .env.local)"
python manage.py runserver "$(sed -n 's/^PORT=//p' .env.local)"
```

**Git.** Keep the generated files out of commits:

```text
# .gitignore
.env.local
.env.test.local
```

## Tell your Workers

A Worker reads your repository's `CLAUDE.md` or `AGENTS.md` before it starts. A few lines there are the lighter alternative to the script, and the only automatic route on the desktop app's own machine. They are best-effort: they work when the Worker follows them, which is not a guarantee the script gives you.

```markdown
## Parallel worktrees

Every ticket worktree has its own databases and its own port, written to
`.env.local` by `.fredrin/setup.sh`.

- If `.env.local` is missing, run `sh .fredrin/setup.sh` from the worktree
  root before anything that touches the database or starts a server.
- Start servers on the port in `.env.local`:
  `bin/rails server -p "$(sed -n 's/^PORT=//p' .env.local)"`.
- Never fall back to port 3000 or to the shared `myapp_development` and
  `myapp_test` databases. If the worktree's own database is unreachable,
  stop and say so instead.
```

## Cleanup

Be aware that nothing drops these databases for you. When a ticket's worktree is removed, its databases stay on your Postgres server until you remove them.

List what has accumulated:

```sh
psql postgres://localhost:5432/postgres -tAc "SELECT datname FROM pg_database WHERE datname ~ '^(test_)?myapp_(dev|test)_'"
```

Drop them all:

```sh
psql postgres://localhost:5432/postgres -tAc "SELECT datname FROM pg_database WHERE datname ~ '^(test_)?myapp_(dev|test)_'" | while read -r db; do psql postgres://localhost:5432/postgres -c "DROP DATABASE \"$db\""; done
```

That also drops the databases of worktrees still in use. In any you keep, run `sh .fredrin/setup.sh` again to recreate them.

## Next steps

- [Branches and worktrees](https://www.fredrin.com/docs/board/worktrees) - what a worktree is, and what it isolates.
- [Running a ticket](https://www.fredrin.com/docs/board/running) - what a run spawns, and how many run at once.
- [Remote agents](https://www.fredrin.com/docs/remote-agent) - run Workers on a machine of your own, where the setup script runs.
