A read-only root filesystem prevents ordinary writes to the filesystem layer containing a service’s packaged code and configuration. Explicit mounts can still provide writable locations for runtime data. This reduces accidental modification and some persistence opportunities, but its boundary is filesystem mutation. It does not by itself remove process privileges, prevent reading available data, or block communication with other services.
When it breaks
Applications often write outside their main data directory: temporary files, caches, lock files, generated configuration, or runtime state can appear during startup and uncommon operations. Making the root read-only can expose those assumptions as EROFS failures. Testing only a health endpoint may miss the path that generates a report, refreshes a cache, or performs shutdown cleanup.
A writable mount remains writable even when the container root layer is read-only. It can contain state that a compromised or faulty process can corrupt. Broadly mounting a writable directory over packaged paths can also defeat the intended protection for those paths. Inspect the effective mount layout rather than inferring the entire pathname tree’s behavior from one root-level option.
Read-only and confidential are different properties. A process may still read packaged files, execute existing programs, and send data through allowed network paths. The setting also does not guarantee protection after a container escape or against authority capable of changing the mount arrangement. Its contribution must be understood alongside credentials, capabilities, isolation, and application behavior.
Check it quickly
Run this Python script as root inside a disposable Linux container started with a read-only root, a writable tmpfs at /tmp, and a separate writable volume at /work. Python 3 must already be in the image. The recorded environment used Linux 6.12.76 and Python 3.11.2, with no host bind mounts, no privileged mode, and no external network:
from pathlib import Path
from tempfile import TemporaryDirectory
import errno,os,socket,subprocess,sys,uuid
assert os.geteuid()==0,'run inside a disposable read-only-root container as root'
assert os.statvfs('/').f_flag&os.ST_RDONLY
name='xbcatm-readonly-'+uuid.uuid4().hex
for base in [Path('/'),Path('/usr'),Path('/etc')]:
path=base/name
try:fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
except OSError as error:assert error.errno==errno.EROFS,error
else:
os.close(fd);path.unlink();raise AssertionError('root filesystem accepted a new file')
print('even this root process receives EROFS on paths in the read-only root, usr and etc tree')
for base in ['/tmp','/work']:
assert not os.statvfs(base).f_flag&os.ST_RDONLY
with TemporaryDirectory(prefix='xbcatm-write-',dir=base) as directory:
path=Path(directory)/'data';path.write_bytes(b'allowed');assert path.read_bytes()==b'allowed'
path.write_bytes(b'changed');assert path.read_bytes()==b'changed'
print('explicit tmpfs and data-volume mounts remain writable, including overwriting their contents')
assert Path('/etc/os-release').read_text()
result=subprocess.run([sys.executable,'-c','print("executed")'],capture_output=True,text=True,check=True)
assert result.stdout.strip()=='executed'
with socket.socket() as listener,socket.socket() as client:
listener.bind(('127.0.0.1',0));listener.listen(1);client.connect(listener.getsockname())
connection,_=listener.accept()
with connection:
client.sendall(b'local');assert connection.recv(5)==b'local'
print('reading packaged files, executing an existing program, and loopback network I/O still work')
The script first checks the root filesystem’s reported read-only flag. It then attempts exclusive creation of uniquely named files under the root, /usr, and /etc. Each fails with EROFS, even for this root identity. Exclusive creation and unique names ensure the probes cannot overwrite a preexisting configuration file.
The fixture next checks that /tmp and /work do not report that flag. Temporary files can be created, read, and overwritten in both mounts. This confirms that writable exceptions remain operational and also establishes their exposure: the root setting does not protect the contents of those deliberately writable locations against the same process.
It reads /etc/os-release and executes an existing Python interpreter successfully. Those results distinguish read-only storage from a policy that blocks reads or execution. Disallowing modification of packaged files can be useful, but it is not a general restriction on what existing code may do with its remaining authority.
Finally, a listener and client exchange a harmless payload over loopback. The container has no external networking, but local socket operations still work. The result shows that the filesystem option does not itself disable networking. It does not establish an external connectivity policy or test communication with any production service.
Every writable probe lives in a temporary directory that is removed afterward. The example verifies selected runtime operations in the actual mount arrangement. It does not claim that every application path has been exercised merely because the root probes fail, nor that arbitrary future writable mounts would inherit the same restriction.
Separate packaged files, scratch space, and persistent state
Choose writable paths from observed application needs and configure the application to use them. Keep the exceptions narrow enough to explain their purpose. A failure should lead to identifying the required path and lifetime, rather than reopening the whole root layer to accommodate one unexpected cache or temporary file.
Use tmpfs for suitable temporary data and account for its memory use and lifecycle. Use a separately managed volume when state needs a lifetime independent of the container’s writable layer, with appropriate backup and recovery. A writable volume supplies a storage location; it does not automatically establish crash durability, confidentiality, or a tested restore procedure.
For systemd-managed services, mechanisms such as ProtectSystem and designated state or runtime directories express related filesystem restrictions with their own documented semantics. Validate the service’s actual view and identity rather than translating an option name mechanically between platforms. Exercise startup, ordinary work, exceptional operations, and cleanup, and confirm both prohibited writes and required writes behave as intended.
Further reading
Sources
- Docker container run reference, read-only root filesystems and separately mounted writable paths, checked 2026-09-12.
- Docker tmpfs mounts, temporary mount storage and lifecycle, checked 2026-09-12.
- Docker volumes, volume lifetime independent of a container root layer, checked 2026-09-12.
- systemd 252 execution environment, ProtectSystem and explicitly writable service directories, checked 2026-09-12.