Skip to Content
Read-Only Root Filesystem With a Writable tmpfs
Advanced
0 likes
Tutorials

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 mentionsUsually needs
/tmp--tmpfs /tmp
/run or a .pid file--tmpfs /run
a cache folder such as /var/cache/... or ~/.cachea tmpfs on that folder
your data foldera 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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.