System.UnauthorizedAccessException: Access to the path '/app/data' is denied.
in a .NET container that runs as USER $APP_UID means the app's user may not
write to /app. Create the folder in the Dockerfile and give it to that user,
or write somewhere the user can write, such as /tmp or a mounted volume.
The tested Dockerfile fix is one line before the USER line:
RUN mkdir /app/data && chown $APP_UID /app/data.
The error
PS> docker run --rm --name fix10-dn-nr fix10-dn-nonroot write
Unhandled exception. System.UnauthorizedAccessException: Access to the path '/app/data' is denied.
---> System.IO.IOException: Permission denied
--- End of inner exception stack trace ---
at System.IO.FileSystem.CreateDirectory(String fullPath, UnixFileMode unixCreateMode)
at System.IO.Directory.CreateDirectory(String path)
at Program.<Main>$(String[] args) in /src/Program.cs:line 12
Why it happens
The final stage ends with USER $APP_UID. In this image, built on
mcr.microsoft.com/dotnet/runtime:10.0, that user is:
PS> docker run --rm --name fix10-dn-nr4 --entrypoint id fix10-dn-nonroot
uid=1654(app) gid=1654(app) groups=1654(app)
WORKDIR /app and COPY came before the USER line,
and the folder they left is owned by root with no write bit for anyone else:
PS> docker run --rm --name fix10-dn-r1 --entrypoint ls fix10-dn-nonroot -ld /app
drwxr-xr-x 1 root root 4096 Nov 13 05:19 /app
The app then called
Directory.CreateDirectory("/app/data") to keep a visit log next to itself,
and Linux refused with "Permission denied", which .NET reports as an
UnauthorizedAccessException with the IOException as the inner
exception. Read both lines: the inner one says it is a permission problem, not a missing
folder, and the id command above, run with --entrypoint, tells
you which user the image runs as before you start debugging the app itself.
The fix
FROM mcr.microsoft.com/dotnet/runtime:10.0
WORKDIR /app
COPY --from=build /out .
RUN mkdir /app/data && chown $APP_UID /app/data
USER $APP_UID
ENTRYPOINT ["dotnet", "ClinicApi.dll"]
PS> docker run --rm --name fix10-dn-nr2 fix10-dn-nonroot-fix write
Wrote /app/data/visits.log
Only the data folder changed owner; /app itself stayed with root:
PS> docker run --rm --name fix10-dn-r2 --entrypoint ls fix10-dn-nonroot-fix -ld /app /app/data
drwxr-xr-x 1 root root 4096 Nov 13 05:20 /app
drwxr-xr-x 2 app root 4096 Nov 13 05:20 /app/data
If the path is configurable, pointing it at /tmp also
worked without changing the image:
PS> docker run --rm --name fix10-dn-nr3 -e DATA_DIR=/tmp/data fix10-dn-nonroot write
Wrote /tmp/data/visits.log
These runs used --rm, so the container and the files it wrote inside it
were removed when it exited. For data you keep, mount a volume at the data path; with
the folder created and
owned as above, a named volume mounted at /app/data was writable in the
read-only test linked below; the same volume test on the image without the
chown line was refused.
How it was reproduced
A console app from dotnet new console -n ClinicApi, no packages, whose
write mode creates a folder (from DATA_DIR, default
/app/data) and appends a line to visits.log. Published in a
multi-stage Dockerfile on mcr.microsoft.com/dotnet/sdk:10.0 (SDK 10.0.302) and
run on mcr.microsoft.com/dotnet/runtime:10.0 (Microsoft.NETCore.App 10.0.13)
with USER $APP_UID, Docker engine 29.9.3 on Windows 11.
Frequently asked
- What user does USER $APP_UID run as in a .NET Docker image?
- In the runtime:10.0 image it is uid 1654, named app, as the id command inside the container printed. In the test /app was owned by root with mode drwxr-xr-x, so this user could not create a folder in it.
- How do I let a non-root .NET container write to a folder?
- Create the folder and change its owner before the USER line, for example RUN mkdir /app/data && chown $APP_UID /app/data, then switch to USER $APP_UID.
- Why does Directory.CreateDirectory throw UnauthorizedAccessException in Docker?
- The process user has no write permission on the parent folder. Linux returned Permission denied and .NET wrapped it in UnauthorizedAccessException, with the IOException as the inner exception.
A container started with --read-only fails on the same write with a different
message: IOException:
Read-only file system. More decoded errors in the
Fixes category.