Architecture & Design

How Docktree orchestrates Docker Compose across multiple git worktrees — and the tradeoffs you need to know before deploying it.

Default isolation: always on, zero config

Without any docktree.yml or platform configuration, every worktree is fully isolated by default. When you run docktree up in a worktree, Docktree:

  1. Allocates a unique set of host ports from the managed pool (41000–49999).
  2. Generates a Compose override that rewrites the project name, container names, and volume names to be unique to that worktree.
  3. Each service defined in your compose file — including databases — gets its own dedicated container. Your postgres in feature-a and your postgres in feature-b are completely separate containers, with separate data volumes.

This is the right default for most cases, especially when worktrees have diverging database schemas (e.g. different migration states). No configuration is required to get this behavior.

System Architecture

Docktree sits between Git and Docker Compose. It reads your existing docker-compose.yml, generates an ephemeral override file for each worktree, and delegates to the Docker Compose engine. It never modifies your source files.

  Git worktree            Docktree                   Docker Engine
  ─────────────           ────────────               ─────────────
  feature-a/    ──up──▶  allocate ports          ──▶  containers-a
  feature-b/    ──up──▶  generate overrides      ──▶  containers-b
                         rewrite env vars
                         manage port registry
  

Platform Tier: when to use it

The Platform Tier is an optional infrastructure optimization. Both the default mode and the platform tier give every worktree a fully isolated database — the difference is only in how that isolation is implemented, not whether it exists.

  • Default (per-worktree containers): Each worktree runs its own dedicated database container. Physical isolation — no server process is shared at all.
  • Platform Tier: A single database server process runs once for the whole repo. Each worktree still gets its own completely isolated namespace inside that server (a separate Postgres DB, a separate Mongo DB, etc.). Your app connects to a DATABASE_URL that points to its own data — nothing leaks between worktrees.

The tradeoff is purely about resource efficiency, not isolation quality.

Use the platform tier when:

  • You have many active worktrees and running a full database per worktree would exhaust host memory or disk.
  • Your database schema is stable across branches (all branches are near the same migration state).
  • Startup speed matters — shared containers don't need to restart between branch switches.

Stick with the default (separate containers per worktree) when:

  • Branches carry diverging migration states — each worktree's database may be at a different schema version. Separate containers make this trivially safe.
  • You want the simplest mental model for destructive schema changes or data wipes — a physical container boundary is easier to reason about than a logical namespace inside a shared server.
  • Your compose stack is lightweight and resource cost is not a concern.
  Default (no platform tier):          Platform Tier (shared infra):

  feature-a ──▶ postgres-a (own)       feature-a ──▶ db_feature_a ─┐
  feature-b ──▶ postgres-b (own)       feature-b ──▶ db_feature_b ─┼──▶ shared postgres
  feature-c ──▶ postgres-c (own)       feature-c ──▶ db_feature_c ─┘

  Both: each worktree connects to its own isolated DATABASE_URL
  Difference: physical container vs. logical namespace in one server

  Memory:    3× database processes     Memory:    1× database process
  Schema:    each DB fully independent Schema:    all DBs on one server version
  

Supported Shared Services & Tenancy Modes

Shared platform services are declared in the shared.services block of docktree.yml. Docktree natively supports the following service kinds:

Service KindSupported TenancyIsolation Mechanism
postgresper_database, full_shareUnique database per worktree. Optionally cloned from a template database for instant seed data / migration state.
mysqlper_database, full_shareUnique database name per worktree.
mongodbper_database, full_shareUnique Mongo database namespace per worktree.
redisper_database, full_shareIsolated Redis logical database indices (0, 1, 2 …) allocated per worktree.
s3per_database, full_shareUnique bucket name per worktree (compatible with localstack / MinIO).
genericfull_shareNo partitioning — all worktrees share the same service instance. Use for stateless services or read-only dependencies.

Dynamic Environment Variable Rewriting

When using per_database tenancy, Docktree detects database connection environment variables declared in your Compose files (e.g. DATABASE_URL, DB_NAME, MONGO_URI) and rewrites them in the generated override so each worktree connects to its own isolated namespace automatically.

This works because Docktree reads the environment directly from your Compose YAML at startup — no runtime agent or sidecar is required.

Gotchas

Secret managers that build DATABASE_URL at runtime

Docktree rewrites environment variables it can see in your Compose files at startup. If your application retrieves secrets from Infisical, Doppler, Vault, AWS Secrets Manager, or any other runtime secret manager that constructs DATABASE_URL dynamically inside a shell command or entrypoint, Docktree cannot safely rewrite it.

In that case, use one of these approaches instead:

  • Preferred: Read the Docktree-provided DATABASE_URL from the container environment first — have the wrapper respect an existing env var before fetching from the vault.
  • Fallback: Skip the platform tier entirely and use isolated per-worktree database containers (the default). No rewriting needed — each worktree has its own DB.

Rule of thumb: If you can see the database URL as a static env var in docker-compose.yml, Docktree can rewrite it. If it's assembled at runtime, it can't.

Branches with diverging migration states

When using the platform tier with per_database tenancy, each worktree gets its own database but they all live inside the same server process. If a branch runs destructive migrations (dropping columns, renaming tables), those changes affect only that branch's database — but mistakes in schema tooling can sometimes touch the wrong database. Test destructive migrations in separate containers (the default mode) rather than the platform tier.

Platform tier must be started from the repo root

The platform stack is repo-scoped, not worktree-scoped. Run docktree platform up from the main repo root, not from inside a worktree. Running it from a worktree directory will likely fail to detect the correct shared services config.

Windows: WSL2 required

Docktree is not supported on native Windows. On Windows, run it inside WSL2 with the Docker Desktop WSL integration enabled. All paths, port allocations, and Compose file detection work correctly under WSL2.

Docktree is not a security sandbox

While Docktree isolates your development environments (giving each worktree its own ports, container names, networks, and volumes), it is not a security sandbox.

All containers still run on the same shared Docker daemon of the host machine. If a container has root access or mounts the Docker socket (/var/run/docker.sock), it retains the same security risks as standard Docker Compose. Docktree prevents operational collisions; it does not isolate system-level permissions or provide kernel-level security boundaries.

Port registry is per-repo

Docktree allocates ports from a registry scoped to the repository root. If you have two separate repositories that both use Docktree, their port ranges will not collide by default — each tracks its own registry in .docktree/. However, if you manually set ports.range to the same range in two different repos on the same machine, collisions are possible.