GitWorktree.org logoGitWorktree.org

Git Worktree with Dev Containers

Dev containers and git worktree both isolate work — but they do it at different layers. Containers isolate the runtime; worktrees isolate the checkout. The combination is powerful, but the mount layout has sharp edges. This guide walks through what actually works, what breaks, and the three settings you need to get right in devcontainer.json.

The Mental Model

A worktree is a separate directory that shares one .git with the main checkout. Inside the worktree directory is a small file (not a directory) called .git that points back to /main-repo/.git/worktrees/<name>.

Containers care about that pointer. If you mount only the worktree into the container, Git inside the container will read the .git file, try to follow the path, and fail to find the actual repo — because that lives outside the mount. This is the single most common bug. Fixing it requires mounting both directories or flattening the worktree into a bare-like layout. We'll cover both.

Strategy A: Mount Both the Worktree and the Main Repo

The simplest pattern. You mount the worktree as the workspace, and you also mount the parent directory (or just the.git) so Git can resolve its pointer:

devcontainer.json — dual mount
{
  "name": "acme-api (worktree)",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",
  "workspaceFolder": "/workspaces/wt-fix-auth",
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/wt-fix-auth,type=bind",
  "mounts": [
    "source=${localWorkspaceFolder}/../../acme-api/.git,target=/workspaces/acme-api/.git,type=bind"
  ]
}

The two paths preserve the exact relationship the worktree expects: /workspaces/wt-fix-auth/.git (the pointer file) → /workspaces/acme-api/.git/worktrees/fix-auth (the real worktree entry). Replace the paths with what your local layout looks like, but keep the relative directory shape.

Pro: Git inside the container behaves exactly like Git outside. Push, pull, fetch, all work.
Con: the mount path is awkward and shaped by your local filesystem. Two developers may need different devcontainer.json files, which is annoying.

Strategy B: Bake the Worktree Setup into postCreateCommand

A different angle: mount the main repo only, and use postCreateCommand to create the worktree inside the container. The worktree never has to cross a mount boundary:

devcontainer.json — worktree created inside
{
  "name": "acme-api (with worktree)",
  "image": "mcr.microsoft.com/devcontainers/javascript-node:20",
  "workspaceFolder": "/workspaces/acme-api",
  "postCreateCommand": "git worktree add ../wt/feature -b feature || true",
  "remoteUser": "node"
}

The worktree lives at /workspaces/wt/feature inside the container. Git resolves it normally because the parent repo is also inside the container.

Pro: simpler devcontainer.json, works across machines.
Con: the worktree exists only inside the container — if you destroy the container you lose the worktree metadata (the branch survives if pushed).

Identity Quirks

Git safe.directory

Mounted Git repos often trigger Git's safe-directory check because the file ownership inside the container does not match the user. You will see:

The dubious ownership error
fatal: detected dubious ownership in repository at '/workspaces/wt-fix-auth'
To add an exception for this directory, call:
        git config --global --add safe.directory /workspaces/wt-fix-auth

Fix it permanently inside your devcontainer.json:

Permanent safe.directory fix
"postCreateCommand": "git config --global --add safe.directory '*'"

Git author identity

Containers don't inherit your host Git config. Either forward it with the official ghcr.io/devcontainers/features/git:1 feature, or set it via remoteEnv:

Forward Git identity into the container
{
  "remoteEnv": {
    "GIT_AUTHOR_NAME": "Your Name",
    "GIT_AUTHOR_EMAIL": "you@example.com",
    "GIT_COMMITTER_NAME": "Your Name",
    "GIT_COMMITTER_EMAIL": "you@example.com"
  }
}

The workspaceFolder Trap

The most expensive mistake when combining devcontainers and worktrees is setting workspaceFolder to a path that does not exist in the container. VS Code shows a confusing “workspace does not exist” error and the container starts but nothing opens. Three things to verify:

  1. The workspaceFolder must be the target side of a workspaceMount (or a mounts entry) bind.
  2. If you use Strategy B, workspaceFolder is the main repo path, not the worktree. Open the worktree via File > Open Folder after the worktree is created.
  3. The directory must exist before the container starts. If your worktree is created by postCreateCommand, don't point workspaceFolder at it.

node_modules, Caches, and Disk

A worktree is a full working directory — that means a fresh node_modules per worktree. Inside a devcontainer this matters more because the container layer is already adding overhead. Two patterns that help:

  • Use pnpm with a shared content store mounted as a Docker volume — see our node_modules guide for the full pattern.
  • Cache the build output (e.g. .next, dist) in a volume keyed by branch, not by worktree path.

The Codex Docker Variant

OpenAI Codex's Docker mode uses a similar pattern: a containerized agent runs inside a worktree the host has already created. For that specific setup, see Codex + Docker + git worktree. The mount layout there is the same Strategy A above with slightly different defaults for the agent's home directory.

FAQ

Should I commit the worktree's devcontainer.json?

Yes — but use Strategy B so the file works on every teammate's machine without local-path edits.

Why does my container fail to find .git?

Almost always a mount issue. The worktree's .git is a pointer file. If the path it points to is not accessible inside the container, every Git command fails. See Strategy A above.

Can I have one container per worktree?

Yes — each worktree gets its own devcontainer.json and its own container. This is the heaviest pattern but the most isolated. Used in production by teams running ephemeral preview environments per branch.

Does this work with GitHub Codespaces?

Codespaces uses devcontainer.json. The same patterns apply, with one caveat: Codespaces mounts only the repo you opened. Worktrees pointing to sibling directories will not resolve. Strategy B (create worktrees inside the container) is the only one that works in Codespaces.

Are there alternatives I should consider first?

If your goal is just per-branch isolation, worktrees with tmux are simpler than devcontainers. Pick devcontainers only when you need true runtime isolation (different Node versions, hostile-code review, regulated environments).

You Might Also Like