Fix root:root Bind-Mount Ownership in VS Code Dev Containers
Fix root:root ownership for bind-mounted files in VS Code Dev Containers on Docker WSL2. Why chown fails and options: WSL2 filesystem, Docker volumes, or rsync.
VS Code Dev Containers on Windows (WSL2 + Docker Desktop): Bind-mounted files owned by root:root, newly created files owned by non-root user (dev)
I’m using VS Code Dev Containers with Docker Desktop on Windows (WSL 2 backend).
Setup:
- Windows 10
- Docker Desktop with WSL 2
Dockerfile:
FROM ghcr.io/astral-sh/uv:0.9.21-python3.13-trixie AS builder
ARG SERVICE_PATH
WORKDIR /workspace/${SERVICE_PATH}
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
ENV UV_PYTHON_DOWNLOADS=0
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=${SERVICE_PATH}/uv.lock,target=uv.lock \
--mount=type=bind,source=${SERVICE_PATH}/pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project
COPY ${SERVICE_PATH}/ .
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked
FROM python:3.13-slim-bookworm
ARG SERVICE_PATH
ARG USERNAME=dev
ARG USER_UID=1000
ARG USER_GID=1000
WORKDIR /workspace/$SERVICE_PATH
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
libffi-dev \
libpq-dev \
make \
curl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --gid $USER_GID $USERNAME \
&& useradd --uid $USER_UID --gid $USER_GID --create-home --shell /bin/bash $USERNAME
COPY --from=ghcr.io/astral-sh/uv:0.9.21 /uv /uvx /bin/
COPY --from=builder --chown=$USERNAME:$USERNAME /workspace/$SERVICE_PATH/ .
ENV PATH=".venv/bin:$PATH"
USER $USERNAME
devcontainer.json:
{
"workspaceFolder": "/workspace/services/users",
"mounts": [
"source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"
],
"remoteUser": "dev"
}
Problem:
All existing files and folders in the bind-mounted workspace appear owned by root:root inside the container, despite the container running as user dev.
Attempts:
COPY --chown=dev:devin Dockerfilechown -RinpostCreateCommand- Running
chownas root or viasudo
None of these change ownership of the existing bind-mounted files from the Windows host.
Observed behavior:
- Deleting and recreating a file/directory inside the container results in
dev:devownership, which persists after container rebuild.
Summary:
- Existing mounted files:
root:root - Newly created files (inside container):
dev:dev
Question:
How can I recursively change ownership of all existing bind-mounted files to the non-root user (dev) in VS Code Dev Containers with WSL2 and Docker Desktop on Windows? What is the recommended best practice for handling file ownership in this setup?
Your files are owned by root because the workspace is being bind-mounted from the Windows-backed filesystem into the Docker engine; with VS Code Dev Containers and Docker WSL2 that Windows-mounted filesystem generally doesn’t honor POSIX ownership, so chown inside the container is ineffective. The reliable fixes are: move or clone the repo into the WSL2 (Linux) filesystem, use a Docker named volume and populate it (so you can chown its contents), or enable WSL automount metadata so chown is honored — then run a recursive chown (or use a postCreateCommand) as needed.
Contents
- Why bind-mounted files appear as root:root (VS Code Dev Containers + Docker WSL2)
- How to change ownership recursively (practical options)
- Best practices for Dev Containers, file ownership, and WSL2 on Windows
- Recipes: move to WSL, populate a volume, or use rsync (step‑by‑step)
- Troubleshooting & FAQ
- Sources
- Conclusion
Why bind-mounted files appear as root:root (VS Code Dev Containers + Docker WSL2)
Short answer: your bind mount is exposing files from a Windows-backed filesystem (e.g., C:\ via /mnt/c) and that filesystem doesn’t provide true Unix UID/GID metadata when mounted into the Docker engine. Docker Desktop’s WSL2-backed engine will present those files inside the container with ownership that the host driver maps (often root:root), even if the container runs as your non-root user (dev). That’s why files created inside the container by the dev user are dev:dev (they’re created by that user inside the container), while files that already exist on the Windows host remain root:root.
A few authoritative notes and references:
- VS Code docs recommend keeping code in the Linux filesystem for performance and correct permissions rather than the Windows filesystem (see the Dev Containers docs and the WSL guidance) — otherwise mounts can behave oddly: https://code.visualstudio.com/docs/devcontainers/containers and https://code.visualstudio.com/remote/wsl.
- The VS Code docs also suggest using a postCreateCommand to update owner if a mount ends up owned by root, but that only works when the underlying filesystem actually supports chown: https://code.visualstudio.com/remote/advancedcontainers/improve-performance.
- This root:root behavior is commonly reported in issues and Docker forums when bind-mounting Windows files: https://github.com/microsoft/vscode-remote-release/issues/5296 and https://forums.docker.com/t/wsl2-docker-desktop-bind-mounts-created-as-root-root-on-host/141322.
How to change ownership recursively for bind mounts (practical options)
You basically have three reliable choices — I’ll list them from most recommended to least.
- Move or clone the repository into the WSL2 (Linux) filesystem — recommended
- Why: files on the WSL2 distribution are real Linux files with POSIX metadata; chown works and is persistent.
- Steps (high level): copy/clone the repo into your WSL distro (e.g., /home/youruser/projects/myrepo), open that folder in VS Code using Remote - WSL, then start the dev container. Once files live in WSL you can run:
- sudo chown -R dev:dev /path/to/project
- or inside the container: sudo chown -R dev:dev /workspace
- Docs hint: the VS Code WSL docs explain using Dev Containers with source stored in WSL: https://code.visualstudio.com/remote/wsl and the community guidance repeatedly points to storing source inside Linux FS for containerized workflows (see https://stackoverflow.com/questions/64010923/developing-inside-docker-on-wsl2-ubuntu-from-vscode).
- Use a Docker named volume and populate it with files that you chown inside a container
- Why: a volume is managed by Docker and is fully POSIX-capable; you can copy the Windows-host files into a volume from a temporary container and chown them there.
- Example workflow (one-off population):
- Create the volume:
- docker volume create my_project_vol
- Populate the volume from your host copy (run this from WSL so you can access /mnt/c path), then chown contents to UID/GID 1000 (dev):
- docker run --rm -v my_project_vol:/workspace -v /mnt/c/Users/You/path/to/repo:/src alpine sh -c “cp -a /src/. /workspace && chown -R 1000:1000 /workspace”
- Update devcontainer.json to mount the named volume:
- “mounts”: [ “source=my_project_vol,target=/workspace,type=volume” ]
- Start the dev container — files in the volume will have the ownership you set.
- Tradeoffs: editing on Windows will not automatically sync into the volume; you need to re-sync or edit inside WSL/container. See the devcontainers docs for volumes vs bind mounts: https://code.visualstudio.com/docs/devcontainers/containers.
- Enable WSL automount metadata (advanced) or copy into WSL then chown
- Why: WSL can be configured to store POSIX metadata for Windows files (so chown/chmod behave more like Linux). If you enable the metadata automount option in WSL and restart it, chown may start working on /mnt/c; after that a chown from the container or WSL will persist.
- Caveats: this changes how drvfs behaves and can have side effects; treat carefully and test. If you prefer not to change automount options, copy the repo into WSL as in option (1).
When chown attempts fail
- If chown returns “Operation not permitted” or chown seems to do nothing, that confirms the bind source is Windows-backed and the operation is unsupported at the mount layer (see the GitHub issue and Docker forum threads linked above). In that case, moving the files into WSL or into a Docker volume is the correct remedy — chown from inside the container won’t change ownership on a Windows-mounted filesystem.
Best practices for Dev Containers, file ownership, and WSL2 on Windows
- Keep code in the WSL2 filesystem for day-to-day development (open the folder with Remote - WSL). That gives correct ownership, much better I/O perf, and avoids root:root bind-mount surprises (see https://code.visualstudio.com/remote/wsl).
- Use Docker volumes for build artifacts, caches, or when you need container-controlled ownership. Volumes are the right place for data that must live with the container and be chowned at container time (see https://code.visualstudio.com/docs/devcontainers/containers).
- Remember build-time chown (COPY --chown=…) only affects image layers; a later bind mount from the host overlays those files — so chown in the Dockerfile won’t change host files that are bind-mounted. That’s why Dockerfile chown didn’t help in your setup.
- Add a postCreateCommand to adjust ownership when the mount actually supports chown. The VS Code docs mention this technique: https://code.visualstudio.com/remote/advancedcontainers/improve-performance. Example fallback in devcontainer.json:
- “postCreateCommand”: “sudo chown -R dev:dev /workspace || true”
- But remember: this only works when the mount supports chown.
- If you must keep code on Windows and want live edits on Windows, look into a file-sync solution (rsync, Unison, Mutagen) that keeps a Linux copy in sync with Windows edits; work in the Linux copy for running containers. That gives you both correct ownership and live-edit convenience.
Recipes: move to WSL, populate a volume, or use rsync (step‑by‑step)
A. Move repo into WSL (quick, recommended)
- Open a WSL shell (Ubuntu) and run:
- mkdir -p ~/projects
- cp -a /mnt/c/Users/
/path/to/repo ~/projects/myrepo - sudo chown -R (id -g) ~/projects/myrepo
- In Windows VS Code: use the Remote - WSL extension to open ~/projects/myrepo and rebuild the dev container.
B. Create and populate a named Docker volume (keeps ownership controlled by Docker)
- docker volume create my_project_vol
- Populate and chown (from WSL so /mnt/c is accessible):
- docker run --rm -v my_project_vol:/workspace -v /mnt/c/Users/
/path/to/repo:/src alpine sh -c “cp -a /src/. /workspace && chown -R 1000:1000 /workspace”
- devcontainer.json snippet:
- {
“workspaceFolder”: “/workspace”,
“mounts”: [“source=my_project_vol,target=/workspace,type=volume”],
“remoteUser”: “dev”,
“postStartCommand”: “sudo chown -R dev:dev /workspace || true”
}
- Start the container. Files in /workspace will be owned by the UID/GID you set.
C. One-off fix when files already live in WSL or a POSIX-capable mount
- Inside WSL or inside the container (when mount supports chown):
- sudo chown -R dev:dev /workspace
- Verify: ls -la /workspace
D. Sync approach (if you insist on editing on Windows)
- Keep a working copy in WSL and use a sync tool to mirror changes from Windows to the WSL copy. Then mount the WSL copy into the container. This gives you correct permissions with live edits.
Troubleshooting & FAQ
Q — Why does COPY --chown in my Dockerfile not fix ownership?
- Because a bind mount from the host overlays the files from the image. The image’s filesystem can be chowned at build time, but when the host directory is bind-mounted into /workspace it hides the image contents and the mount’s metadata governs ownership.
Q — Why can I create files as dev (they show dev:dev) but existing ones are root:root?
- New files are created by the process running as dev inside the container and get dev ownership. Existing files created on the Windows host or by Docker Desktop’s mount driver keep the host/backing metadata (root:root).
Q — I ran sudo chown -R, but ownership didn’t change — what now?
- That means the backing filesystem doesn’t support POSIX ownership changes (typical for Windows-mounted drives). Use one of the reliable options above: move files to WSL, copy into a Docker volume and chown there, or enable WSL automount metadata and retry.
Q — Where can I read more or see similar reports?
- See these community reports and official docs: https://github.com/microsoft/vscode-remote-release/issues/5296, https://github.com/microsoft/vscode-dev-containers/issues/277, and https://forums.docker.com/t/wsl2-docker-desktop-bind-mounts-created-as-root-root-on-host/141322. The VS Code Dev Containers docs and WSL docs also discuss these topics: https://code.visualstudio.com/docs/devcontainers/containers and https://code.visualstudio.com/remote/wsl.
Sources
- Improve disk performance (postCreateCommand mention)
- Developing inside a Container (volumes vs bind mounts)
- vscode-remote-release Issue: bind mounted files are owned by root even if container using a non-root user
- Dev Containers Tips and Tricks
- Add a non-root user to a container (note about mounted files)
- Using Dev Containers in WSL 2 (blog)
- StackOverflow: bind mount to a vscode devcontainer without hard-coding it
- StackOverflow: Developing inside Docker on WSL2-Ubuntu from VS Code
- devpod Issue: Folder to wsl2 is mounted from windows filesystem
- Add another local file mount
- Docker Community Forum: WSL2 + Docker Desktop, Bind mounts created as root:root on host
- vscode-dev-containers Issue: Mounted docker socket needs root user to read from
- Dev Containers Part 3: UIDs and file ownership (community blog)
- Developing in WSL (VS Code)
Conclusion
Because Windows-backed bind mounts don’t expose POSIX ownership to the container, you can’t reliably chown those files from inside the container; this is a known behavior with VS Code Dev Containers and Docker WSL2. The simplest, safest fix is to keep the repository in the WSL2 (Linux) filesystem or copy it into a Docker volume and chown there; if needed, enable WSL automount metadata so chown is honored. Those approaches give correct, persistent dev:dev ownership and avoid the root:root surprises.