In a dev container, containerUser sets the user for everything inside the container, while remoteUser sets the user for your connected tools, terminals and debugging only. If you set neither, they often run as root. Setting a non root remoteUser is the simplest improvement most projects can make.
What each property does
The containers.dev reference defines both:
| Property | What it overrides | Default |
|---|---|---|
| remoteUser | The user that the tools run as, including terminals, tasks and debugging, with their sub processes. It does not change the user the container as a whole runs as | The user the container runs as, often root |
| containerUser | The user for all operations run inside the container | Either root or the last USER instruction in the related Dockerfile |
A third property, updateRemoteUserUID, applies on Linux when either user is set. It updates that user's UID and GID to match your local user's, to avoid permission problems with bind mounts. It defaults to true.
Why root as the default is a problem
Dev container setup is full of code you did not write: extensions, post create scripts, package install hooks and Feature install scripts. If your terminal and tools run as root inside the container, any of those has the strongest privileges the container allows, including the ability to modify system paths and, with bind mounts, host files owned by root or readable by root.
Running as a non root user does not make an escape impossible, but it removes the easy wins. It also surfaces permission mistakes that would otherwise appear only in production, where containers should not run as root either. For the production side, see running a container as a non root user.
How to set it up
Use a base image that already has a non root account, or create one in the Dockerfile, then point the dev container at it:
{
"image": "IMAGE_WITH_A_DEV_USER",
"remoteUser": "dev"
}
The image and the account name are placeholders. If you need root for one setup step, keep that step in the image build or a Feature, and let day to day work run as the dev user.
Features and the root exception
The Features reference says Feature install scripts run as the root user. That is by design, so they can install tools system wide. The scripts receive the container user and remote user as environment variables so they can set up the right home folder. The practical consequence: root is used during setup, and your daily session does not have to be.
Common problems when you drop root
- Cannot write to the workspace. The updateRemoteUserUID default helps on Linux. Check the folder's ownership otherwise.
- Tools cannot install global packages. Install them during the image build instead of at runtime.
- Privileged ports. A non root process cannot bind below 1024 without the right capability. Use a higher port.
- Docker in Docker. Needs privileges. Decide whether you truly need it, and see what privileged mode costs.
A checklist
- Set
remoteUserto a non root account in every dev container. - Do any root work in the image build, where it is pinned and can be scanned.
- Keep
updateRemoteUserUIDat its default unless you have a reason. - Review any Feature that changes users or privileges.
For the full settings review, see what a code reviewer should flag.
Safeguard can scan the images and packages your dev container is built from, with a free scan at gold.safeguard.sh/scan.
Quick check
Does remoteUser change the user the container runs as?
No. The reference says it affects the user for tools and their sub processes, not the container as a whole. Use containerUser for that.
Do Feature install scripts run as my remote user?
No. The Features reference says they run as root during installation.
Sources
Last reviewed: 19 October 2026.