System.IO.IOException: Read-only file system : '/app/data/visits.log' from a
.NET app in Docker means the container was started with --read-only and the
app wrote to a path that is part of the image. Give that path writable storage: a
--tmpfs mount for scratch files or a volume for data you keep.
Both were tested: --tmpfs /tmp with the app pointed at /tmp/data,
and a named volume mounted at /app/data. Each made the same write succeed with
--read-only still on.
The error
PS> docker run --rm --name fix10-dn-ro --read-only fix10-dn-nonroot-fix write
Unhandled exception. System.IO.IOException: Read-only file system : '/app/data/visits.log'
at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirError)
...
at System.IO.File.WriteToFile(String path, FileMode mode, ReadOnlySpan`1 contents, Encoding encoding)
at Program.<Main>$(String[] args) in /src/Program.cs:line 15
Why it happens
--read-only makes the container's own file system read-only. The image used
here has a /app/data folder owned by app
(drwxr-xr-x 2 app root in an ls -ld), and the same image wrote
/app/data/visits.log without --read-only. With the flag, the
write failed. The app's call to
File.AppendAllText on line 15 is where the exception surfaced.
/tmp is not an exception on its own. Pointing the app at /tmp/data
without a tmpfs mount failed the same way, one step earlier, when it created the folder:
PS> docker run --rm --name fix10-dn-ro4 --read-only -e DATA_DIR=/tmp/data fix10-dn-nonroot-fix write
Unhandled exception. System.IO.IOException: Read-only file system : '/tmp/data'
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
The fix
docker run --rm --name fix10-dn-ro2 --read-only --tmpfs /tmp -e DATA_DIR=/tmp/data fix10-dn-nonroot-fix write
Wrote /tmp/data/visits.log
docker run --rm --name fix10-dn-ro3 --read-only -v fix10-dn-data:/app/data fix10-dn-nonroot-fix write
Wrote /app/data/visits.log
--tmpfs /tmp gives the container a writable /tmp mount of its own;
use it for scratch files the app can lose. The named volume fix10-dn-data is a
writable mount at exactly the path the app writes to, and the rest of the file system stays
read-only. The owner of the image's folder mattered: a volume created with
docker volume create and mounted at /app/data was writable on
this image, which chowns the folder to app, and the same test on an image
without that line was refused with UnauthorizedAccessException: Access to the path
'/app/data/visits.log' is denied.
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 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) as
USER $APP_UID, with RUN mkdir /app/data && chown $APP_UID
/app/data in the Dockerfile. Docker engine 29.9.3 on Windows 11. The container,
volume and image names in the commands are the test's own.
Frequently asked
- How do I give a read-only .NET container a writable folder?
- Add --tmpfs /tmp for scratch files, or mount a volume at the exact path the app writes to, for example -v app-data:/app/data. Both made the write succeed in the test while --read-only stayed on.
- Is /tmp writable in a Docker container started with --read-only?
- Not in this test. Creating /tmp/data failed with Read-only file system until the container was started with --tmpfs /tmp.
- Why does my .NET app get IOException Read-only file system in Docker?
- The container was started with --read-only, so the image's files cannot be changed. Any path the app writes to needs a tmpfs or volume mount.
Without --read-only but as a non-root user, the same write can fail on
permissions instead:
Access
to the path is denied. More decoded errors in the
Fixes category.