Docker Compose with Worktrees

The problem

Docker Compose maps container ports to host ports. When multiple worktrees run docker compose up, every instance tries to bind the same host ports — only the first one succeeds.

How Docker Compose reads .env

Docker Compose automatically loads a .env file from the working directory where you run it. Since each Rift worktree is its own directory, each worktree gets its own .env — no extra configuration needed.

Setup

docker-compose.yml

Use ${VAR:-default} syntax so the file works both with and without a .env:

services:
  app:
    build: .
    ports:
      - "${PORT:-3000}:3000"

  db:
    image: postgres:16
    ports:
      - "${DB_PORT:-5432}:5432"
    environment:
      POSTGRES_PASSWORD: postgres

  redis:
    image: redis:7
    ports:
      - "${REDIS_PORT:-6379}:6379"

The left side of each port mapping (the host port) is the one that must be unique. The right side (the container port) stays the same — your application code always connects to the standard port inside the container.

bootstrap.sh

Here’s the multiple ports variant to generate a .env with all the ports your services need:

#!/usr/bin/env bash
set -euo pipefail

hash=$(echo -n "$RIFT_WORKTREE" | shasum | tr -d 'a-f ' | cut -c1-4)
BASE=$(( (hash % 7000) + 3000 ))

cat > .env <<EOF
PORT=$BASE
DB_PORT=$(( BASE + 1 ))
REDIS_PORT=$(( BASE + 2 ))
EOF

echo "Assigned ports $BASE, $(( BASE + 1 )), $(( BASE + 2 )) for worktree '$RIFT_WORKTREE'"

rift.yaml

For Docker Compose projects, add container lifecycle commands to the hooks in your rift.yaml:

hooks:
  open: "bash scripts/bootstrap.sh && docker compose up -d"
  jump: "bash scripts/bootstrap.sh"
  close: "docker compose down"
  purge: "docker compose down -v"

The bootstrap command can be anything — bash scripts/bootstrap.sh, npm run bootstrap, etc. See Hooks for details.

  • open — generate ports, then start containers in the background.
  • jump — regenerate .env (containers keep running from the previous session).
  • close — stop and remove containers.
  • purge — stop containers and remove volumes (full cleanup).

Container naming

Docker Compose derives the project name from the directory name. Since each worktree lives in its own directory (e.g. bold-ant), containers are automatically namespaced:

bold-ant-app-1
bold-ant-db-1
bold-ant-redis-1

No COMPOSE_PROJECT_NAME override is needed.

Internal vs external ports

The host port (external) changes per worktree. The container port (internal) stays the same.

Host (unique per worktree)     Container (always the same)
        4521  ──────────────►  3000   (app)
        4522  ──────────────►  5432   (postgres)
        4523  ──────────────►  6379   (redis)

Your application code inside the container always connects to localhost:5432 for Postgres, localhost:6379 for Redis, etc. Only external access (your browser, API clients) uses the worktree-specific port.

Accessing services

To check which ports a worktree is using:

cat .env

Or ask Docker directly:

docker compose ps

Cleanup

The close and purge hooks handle teardown automatically. If a worktree is removed manually (e.g. by deleting the directory), its containers may be left running. Clean them up with:

docker compose -p <worktree-name> down -v

Or remove all stopped containers:

docker container prune