Read-Only Root Filesystem With a Writable tmpfs
Find where an app writes, run it read-only, and give it a tmpfs for scratch files and a volume for data. After this, nothing inside the container can change its own files.
With --read-only, nothing inside the container can change the image's files: no dropped scripts, no edited config, no new binaries. Most apps still need to write somewhere, so you give them a small tmpfs (memory) mount for scratch files and a volume for real data. This page locks down a small Python app and shows how to find the paths it needs. Run on Docker Desktop 4.85.0 (Engine 29.6.2) with python:3.14-alpine.
The app
It writes a temporary file on every start and keeps a counter in a data folder.
import pathlib, tempfile
with tempfile.NamedTemporaryFile(dir="/tmp", delete=True) as f:
f.write(b"scratch")
print("temp file ok:", f.name.startswith("/tmp/"))
counter = pathlib.Path("/data/count.txt")
n = int(counter.read_text()) + 1 if counter.exists() else 1
counter.write_text(str(n))
print("start number", n)
FROM python:3.14-alpine
RUN mkdir /data
COPY app.py /app.py
CMD ["python", "/app.py"]
Step 1: find where it writes
Run it normally once, then ask Docker what changed in the container:
Set-Location ro-app
docker build -q -t ro-app . | Out-Null
docker run --name ro-probe ro-app
$diff = docker diff ro-probe
$diff | Where-Object { $_ -notlike "*/usr/local/lib/python3.14/*" }
"entries under /usr/local/lib/python3.14/: " + ($diff | Where-Object { $_ -like "*/usr/local/lib/python3.14/*" }).Count
temp file ok: True
start number 1
C /data
A /data/count.txt
C /usr
C /usr/local
C /usr/local/lib
C /usr/local/lib/python3.14
entries under /usr/local/lib/python3.14/: 51
There are two kinds of change. The app wrote its counter file in /data, and Python wrote its bytecode cache (__pycache__ folders, the entries I counted instead of listing) under /usr/local/lib. The cache is optional: on a read-only file system Python skips it without an error. The counter is not optional, so /data needs a writable place. The temp file was deleted before the app ended, so it is not in the list. That is a known limit of docker diff: it shows what is left, not everything that was written. Check the app's code or documentation for temp folders too.
Step 2: read-only, and see what breaks
docker run --rm --read-only ro-app 2>&1 | ForEach-Object { "$_" } | Select-String "Error|error"
newline=newline, encoding=encoding, errors=errors,
OSError: [Errno 30] Read-only file system: '/tmp/tmp6tjwmfjs'
The first write failed, so the app stopped there. Read-only is doing its job.
Step 3: tmpfs for scratch, volume for data
docker run --rm --read-only --tmpfs /tmp:size=16m -v ro-data:/data ro-app
docker run --rm --read-only --tmpfs /tmp:size=16m -v ro-data:/data ro-app
temp file ok: True
start number 1
temp file ok: True
start number 2
The temp file goes to memory and disappears with the container. The counter goes to the named volume and survives, which is why the second run says 2. Everything else in the container is locked.
Step 4: check that it is really locked
docker run --rm --read-only --tmpfs /tmp:size=16m -v ro-data:/data ro-app sh -c "echo 'print(1)' > /app.py; echo exit code `$?"
exit code 1
sh: can't create /app.py: Read-only file system
The same in Compose
services:
app:
image: ro-app
read_only: true
tmpfs:
- /tmp:size=16m
volumes:
- ro-compose-data:/data
volumes:
ro-compose-data:
Set-Location ..
docker compose -f ro-compose/compose.yaml run --rm app 2>$null
docker compose -f ro-compose/compose.yaml down -v 2>$null
temp file ok: True
start number 1
Common causes when an app fails under --read-only
| Error mentions | Usually needs |
|---|---|
/tmp | --tmpfs /tmp |
/run or a .pid file | --tmpfs /run |
a cache folder such as /var/cache/... or ~/.cache | a tmpfs on that folder |
| your data folder | a volume, not a tmpfs |
⚠ Warning: tmpfs uses memory. Without size=, it can grow up to half of the machine's memory, so always set a size. Anything on a tmpfs is gone when the container stops.
Related course: Docker Advanced.
About
Find where an app writes, run it read-only, and give it a tmpfs for scratch files and a volume for data. After this, nothing inside the container can change its own files.
- Responsible
- Jay P
- Last Update
- 08/09/2026
- Reading Time
- 4 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.