Skip to content

Docker Compose

Grove's Docker integration gives each worktree an isolated Compose stack — unique project name, unique ports, and optional shared infrastructure for databases and caches.

How it works

Grove reads .env.worktree to discover the Compose project name and port bindings for each worktree. It then uses docker compose -p <project> to keep stacks isolated at the Docker level.

When you run grove start, Grove:

  1. Creates the worktree directory
  2. Bootstraps .env from .env.example when .env is missing
  3. Generates or refreshes .env.worktree from naming templates (if .grove/config.json exists)
  4. Starts the shared infrastructure stack (if configured)
  5. Runs docker compose up -d --build for the worktree

Setup

Step 1 — Run grove setup

bash
grove setup

Grove detects your docker-compose.yml or compose.yaml and proposes a config. Confirm and it writes .grove/config.json.

Step 2 — Review the config

.grove/config.json for a typical Docker project:

json
{
  "project": "my-app",
  "providers": {
    "web": { "type": "docker-compose", "service": "web" },
    "api": { "type": "docker-compose", "service": "api" }
  },
  "naming": {
    "composeProject": "${project}-${branch_safe}",
    "dbSchema": "${project}_${branch_safe}",
    "ports": {
      "WEB_PORT": "auto",
      "API_PORT": "auto",
      "DB_PORT": "auto"
    }
  }
}

Naming template variables:

VariableExpands to
${branch}Branch name as-is (e.g. feat/auth-refresh)
${branch_safe}Branch lowercased, /-, max 40 chars (e.g. feat-auth-refresh)
${project}Project name from config

Step 3 — Wire ports in your Compose file

Use the Grove-generated env vars in your compose.yaml:

yaml
services:
  web:
    build: .
    ports:
      - "${WEB_PORT}:3000"
  db:
    image: postgres:16
    ports:
      - "${DB_PORT:-5432}:5432"
    environment:
      POSTGRES_DB: ${DB_SCHEMA}

Grove writes WEB_PORT, DB_PORT, DB_SCHEMA, and COMPOSE_PROJECT_NAME into .env.worktree. Your Compose file reads them.

Shared infrastructure

Some services (Postgres, Redis) run once across all worktrees. Grove manages these as a separate "shared stack":

Compose file layout

compose.yaml           # per-worktree: web, api, worker
compose.shared.yaml    # shared: db, redis

compose.shared.yaml — runs once under a fixed project name:

yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: dev
    volumes:
      - db_data:/var/lib/postgresql/data
  redis:
    image: redis:7-alpine
volumes:
  db_data:

compose.yaml — per-worktree, joins the shared network:

yaml
services:
  web:
    build: .
    ports:
      - "${WEB_PORT}:3000"
    environment:
      DATABASE_URL: postgres://postgres:dev@db:5432/${DB_SCHEMA}
      REDIS_URL: redis://redis:6379
    networks:
      - default
      - shared_net

networks:
  shared_net:
    external: true
    name: ${SHARED_PROJECT_NAME}_default

Docker Compose creates a network named <project>_default for every project. Per-worktree services join the shared project's network as an external network — that's how they reach db and redis by hostname.

Config for shared infrastructure

json
{
  "shared": { "db": true, "redis": true },
  "sharedComposeFile": "compose.shared.yaml",
  "naming": {
    "sharedProject": "my-app-shared",
    "composeProject": "my-app-${branch_safe}"
  }
}

naming.sharedProject is required to enable shared stack management. Grove writes SHARED_PROJECT_NAME=my-app-shared into every .env.worktree.

Managing the shared stack

bash
grove shared up       # start (idempotent — safe to run if already running)
grove shared status   # show state
grove shared down     # stop

grove start automatically starts the shared stack if it isn't running.

Env contract

For projects with secrets or derived env vars, configure envContract in .grove/config.json:

json
{
  "envContract": {
    "strict": true,
    "passthrough": ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_DB"],
    "derived": {
      "DATABASE_URL": "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?schema=${DB_SCHEMA}"
    },
    "required": ["DATABASE_URL"],
    "sourceEnvFiles": [".env"]
  }
}
ClassBehavior
passthroughCopied from source env files (.env, .env.example by default) into .env.worktree
derivedRendered from explicit templates — never guessed
requiredMust be present after rendering — fails fast if missing

Diagnostics

If Docker seems to ignore Grove's ports:

bash
# Regenerate .env.worktree for a worktree
grove start feat/my-feature --refresh-env

# Inspect what Compose expects vs what Grove generated
grove doctor env feat/my-feature

grove doctor env shows expected variables, detected port references, and any mismatches.

Custom startup scripts

Sometimes docker compose up is not enough. If your startup sequence needs steps that plain Compose cannot express — waiting for a database to be healthy, running per-worktree schema migrations, seeding fixture data on first boot — use the custom-shell provider.

How it works

Grove still generates .env.worktree exactly as it would for a docker-compose project. Instead of calling docker compose up itself, it:

  1. Bootstraps .env from .env.example when .env is missing.
  2. Generates or refreshes .env.worktree with Grove-owned values.
  3. Invokes your script with env precedence: shell env > .env.worktree > .env > .env.example.
  4. Sets GROVE_ENV_FILE=<worktree-path>/.env.worktree so the script can forward the file to docker compose --env-file "$GROVE_ENV_FILE".
  5. Runs your script with cwd set to the worktree root.

Your script is responsible for the actual docker compose up call and any surrounding logic.

Config

json
{
  "project": "my-app",
  "providers": {
    "web": {
      "type": "custom-shell",
      "service": "web",
      "script": "bin/start.sh",
      "stopScript": "bin/stop.sh"
    }
  },
  "naming": {
    "composeProject": "${project}-${branch_safe}",
    "dbSchema": "${project}_${branch_safe}",
    "ports": {
      "WEB_PORT": "auto",
      "DB_PORT": "auto"
    }
  }
}
FieldRequiredDescription
scriptyesPath to the startup script, relative to the worktree root
stopScriptnoPath to the teardown script. Falls back to docker compose down when absent.
servicenoReserved — not currently used by the runtime

Writing the script

Use this baseline for startup scripts to keep ownership and precedence explicit:

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

# Ensure base local env exists.
if [ ! -f .env ] && [ -f .env.example ]; then
  cp .env.example .env
fi

# Grove should have generated this.
if [ ! -f .env.worktree ]; then
  echo "Missing .env.worktree. Run through grove start." >&2
  exit 1
fi

# Precedence: shell env > .env.worktree > .env > .env.example
set -a
[ -f .env ] && . ./.env
. ./.env.worktree
set +a

GROVE_ENV_FILE="${GROVE_ENV_FILE:-.env.worktree}"

COMPOSE_FLAGS=(-p "$COMPOSE_PROJECT_NAME" --env-file "$GROVE_ENV_FILE")

# Start the database and wait for its healthcheck.
docker compose "${COMPOSE_FLAGS[@]}" up -d db
docker compose "${COMPOSE_FLAGS[@]}" wait db

# Run migrations once per worktree.
SEED_MARKER=".grove/.seeded-${COMPOSE_PROJECT_NAME}"
if [[ ! -f "$SEED_MARKER" ]]; then
  docker compose "${COMPOSE_FLAGS[@]}" run --rm db \
    psql -U app -c "CREATE SCHEMA IF NOT EXISTS \"$DB_SCHEMA\";"
  touch "$SEED_MARKER"
fi

# Bring up the application.
docker compose "${COMPOSE_FLAGS[@]}" up -d --build web
echo "Ready at http://localhost:${WEB_PORT}"

Variables available in the script (from merged env files and shell):

VariableExample value
COMPOSE_PROJECT_NAMEmy-app-feat-login
WEB_PORT8081
DB_PORT5433
DB_SCHEMAmy_app_feat_login
GROVE_ENV_FILE/home/user/worktrees/feat-login/.env.worktree

Any additional variables from naming.ports or envContract.passthrough / envContract.derived are also present.

Stop script

If stopScript is omitted, grove stop falls back to docker compose -p $COMPOSE_PROJECT_NAME down. Provide a stop script when you need custom teardown:

bash
#!/usr/bin/env bash
set -euo pipefail
docker compose -p "$COMPOSE_PROJECT_NAME" --env-file "$GROVE_ENV_FILE" down

Full example

See the Docker Script example for a complete project using this pattern.

Docker commands

bash
grove docker up feat/my-feature        # docker compose up -d --build
grove docker down feat/my-feature      # docker compose down
grove docker teardown feat/my-feature  # docker compose down -v (destroys volumes)

teardown prompts you to type the project name before proceeding — it's destructive.

Released under the MIT License.