Nested devcontainer.json files in a polyglot monorepo bind-mount overlapping host directories, triggering ENOSPC (inode exhaustion) and EACCES (permission denied) the moment pnpm install runs. This page fixes the mount conflict and standardizes a single root configuration; it extends the devcontainer configuration standards within the broader containerized local environment patterns.

Diagnostic

When service-level devcontainer.json files override the root workspaceMount, Docker mounts both the monorepo root and a nested package path. The overlapping recursive bind mapping exhausts inodes or denies permission during dependency resolution. Inspect the active bind mounts:

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

docker compose -f .devcontainer/docker-compose.yml config --quiet
docker inspect "$(docker ps -q -f name=devcontainer)" \
  --format '{{json .Mounts}}' | jq '.[] | select(.Type=="bind")'

Expected BAD output — note the duplicated root and nested bind paths:

[
  {
    "Type": "bind",
    "Source": "/Users/dev/projects/monorepo/packages/web",
    "Destination": "/workspace/packages/web",
    "RW": true,
    "Propagation": "rprivate"
  },
  {
    "Type": "bind",
    "Source": "/Users/dev/projects/monorepo",
    "Destination": "/workspace",
    "RW": true,
    "Propagation": "rprivate"
  }
]

A second tell is high overlay utilization from the redundant mounts:

#!/usr/bin/env bash
set -euo pipefail
grep -rE 'workspaceFolder|workspaceMount' .devcontainer/ --include='*.json'
docker run --rm -v "$(pwd):/tmp" alpine df -h /tmp
.devcontainer/web/devcontainer.json:  "workspaceMount": "...monorepo..."
.devcontainer/api/devcontainer.json:  "workspaceMount": "...monorepo..."
Filesystem  Size  Used Avail Use% Mounted on
overlay      59G   45G   14G  77% /tmp

Root Cause

Each nested devcontainer.json redeclares workspaceMount against the monorepo root, so Docker stacks one bind mount inside another. The recursive inode mapping exhausts the device (ENOSPC), and because the nested service runs as a different user than the root mount owner, writes into node_modules fail with EACCES. Hardcoded absolute host paths make this worse across macOS, Linux, and Windows because the same config resolves to different real directories per host.

Resolution

  1. Remove nested workspaceMount overrides. Strip explicit workspaceMount declarations from every service-level devcontainer.json and let the root configuration propagate the single bind mount.

  2. Define one root configuration at .devcontainer/devcontainer.json that owns the mount and the Compose file:

    // .devcontainer/devcontainer.json
    {
      "workspaceFolder": "/workspace",
      "dockerComposeFile": ["docker-compose.yml"],
      "service": "dev",
      "postCreateCommand": "pnpm install --frozen-lockfile && pnpm run build"
    }
  3. Map toolchain caches to named volumes, not bind mounts, so polyglot package stores bypass the host I/O path:

    # .devcontainer/docker-compose.yml
    services:
      dev:
        build: .
        volumes:
          - ..:/workspace:cached
          - pnpm_store:/workspace/.pnpm-store
          - go_cache:/workspace/.cache/go
    volumes:
      pnpm_store:
      go_cache:
  4. Inject toolchain roots via remoteEnv instead of hardcoded host paths, keeping resolution identical across operating systems:

    // .devcontainer/devcontainer.json
    {
      "remoteEnv": {
        "NX_WORKSPACE_ROOT": "/workspace",
        "PNPM_HOME": "/workspace/.pnpm-store",
        "GOPATH": "/workspace/.cache/go"
      }
    }
  5. Validate before committing with devcontainer config --workspace-folder . to confirm a single resolved workspaceMount.

Expected Output

After consolidation, a trace-level bring-up shows exactly one workspace bind mount and named-volume caches:

#!/usr/bin/env bash
set -euo pipefail
devcontainer up --workspace-folder . --log-level trace 2>&1 \
  | grep -E 'workspaceMount|volume|network'
workspaceMount: source=/Users/dev/projects/monorepo,target=/workspace,type=bind
volume: monorepo_pnpm_store:/workspace/.pnpm-store
network: monorepo_net attached to container
devcontainer up completed successfully

Prevention

  • Maintain a central .devcontainer/base/devcontainer.json and have services reference it instead of redeclaring mounts.

  • Add a PR check that rejects any nested workspaceMount key:

    #!/usr/bin/env bash
    set -euo pipefail
    if grep -rl '"workspaceMount"' .devcontainer/*/devcontainer.json 2>/dev/null; then
      echo "ERROR: nested workspaceMount override detected" >&2
      exit 1
    fi
    echo "mount strategy OK"
  • Pin base images to SHA digests (node:20.11.0-alpine@sha256:...) so cache state is reproducible, per the devcontainer configuration standards.

macOS (Docker Desktop): Inode pressure surfaces faster because each FUSE-translated bind mount carries overhead; named volumes for caches sidestep it entirely. WSL2: Keep the monorepo on the Linux filesystem (~/code, not /mnt/c) — 9p translation multiplies the cost of the duplicate mounts. Apple Silicon (ARM64): Confirm every base image has an arm64 manifest with docker manifest inspect; an emulated amd64 toolchain compounds the slow-mount problem during pnpm install.

Rollback

#!/usr/bin/env bash
set -euo pipefail
git checkout HEAD -- .devcontainer/
docker compose -f .devcontainer/docker-compose.yml down -v
devcontainer up --workspace-folder . --remove-existing-container