Skip to main content
CleanStart

Knowledge Hub

Read-Only Filesystem Per-Application Guide

What Is Read-Only Root Filesystem

A read-only root filesystem is a security hardening technique that prevents write access to the container's entire filesystem except for explicitly declared writable paths. This means your application code (/app, /opt, /usr) is immutable at runtime, system files (/etc, /bin, /lib) are protected from modification, and only designated directories (/tmp, /var/run, etc.) are writable. Attackers cannot create backdoors, install tools, or modify application behavior.

Why It Matters

Immutability is the foundation of container security. Once your container starts with a read-only filesystem, nothing critical can change during runtime. The code you deployed is the code that runs—no runtime modifications are possible. Attackers cannot install backdoors by writing to disk. Malware cannot persist because it has nowhere to write. Runtime supply chain attacks that modify application behavior at execution time become impossible. This immutability transforms the security model from "hope we detect and respond to attacks" to "attacks of this class cannot occur by design."

Tamper detection becomes automatic. Any attempt to write to a protected path immediately fails with a "Read-only file system" error. This error is auditable—it appears in logs and can trigger alerts. If a compromised container attempts to write a backdoor to /bin or modify /etc/passwd, the write fails immediately, the failure is logged, and security systems can detect the intrusion attempt in real time. Traditional containers allow these modifications silently, enabling attackers to establish persistence. Read-only filesystems reject the attack before persistence is possible.

Compliance frameworks explicitly require this protection. PCI-DSS (payment card industry standard) requires containers handling sensitive data to enforce filesystem integrity. FedRAMP (government cloud security) mandates immutable containers for federal systems. DISA STIG (Department of Defense security standard) requires read-only root filesystems. CIS Benchmarks explicitly score read-only filesystems as a security control. These are not suggestions—they are compliance requirements for regulated organizations. Read-only filesystems are the mechanism to satisfy these requirements.

Operational simplicity eliminates an entire class of bugs. In traditional containers with writable filesystems, applications sometimes write to unexpected locations: temporary files, log files, cache files in locations the developer didn't anticipate. These writes create technical debt—disk space is consumed, garbage accumulates, file permissions become incorrect. Read-only filesystems prevent this class of problem entirely by forcing developers to explicitly declare where applications need to write. The explicit declaration becomes documentation. The failures become visible in development rather than manifesting silently in production.

How It Works

Docker (--read-only): At runtime, the container launches with the entire filesystem mounted as read-only.

docker run --read-only --tmpfs /tmp --tmpfs /var/run myapp:latest

Kubernetes (readOnlyRootFilesystem: true): The kubelet enforces read-only root at the pod level.

securityContext:  readOnlyRootFilesystem: true

Any write to a protected path immediately fails with "Read-only file system" error, preventing the application from continuing.

The Writable Path Inventory

Every CleanStart application image requires specific directories to be writable. This complete table documents which paths each application needs and why:

Application

Writable Paths

Purpose

Volume Type

Size

Notes

PostgreSQL

/var/lib/postgresql/data

Database files, WAL logs, indexes

PersistentVolumeClaim

Variable (10GB+)

CRITICAL: Must persist across restarts


/var/run/postgresql

Unix socket, PID file

emptyDir

<1MB

Can be ephemeral


/tmp

Temp files, query spills

emptyDir

1GB

May grow with large queries

Redis

/data

RDB snapshots, AOF logs

PersistentVolumeClaim

Variable (depends on data)

Optional persistence


/tmp

Lua script temp, memory ops

emptyDir

<100MB

Rarely used

Kafka

/var/lib/kafka/data

Log segments (critical)

PersistentVolumeClaim

Variable (100GB+)

DO NOT use emptyDir — data loss


/tmp

Temp files during compaction

emptyDir

10GB+

May spike during log rotation

Nginx

/var/cache/nginx

Caching, buffering

emptyDir

1-10GB

Performance-critical


/var/run

PID file, socket

emptyDir

<1MB

Ephemeral, created at startup


/tmp

Temp files, uploads

emptyDir

1-5GB

Request temporary storage

Python

/tmp

Package install, temp files

emptyDir

1GB

Virtual env, pip cache


/var/cache/pip

Pip package cache (optional)

emptyDir

Variable

Optional for faster reinstalls

Node.js

/tmp

NPM/Yarn temp, module cache

emptyDir

2-5GB

Npm native modules compilation


$HOME/.npm

NPM cache (optional)

emptyDir

Variable

Optional, for faster installs

Java

/tmp

JVM temp files, heap dumps

emptyDir

2-8GB

Critical for JVM operation


/var/cache/java

Compiled class cache (optional)

emptyDir

Variable

Performance optimization

Go

/tmp

Go build cache (rarely needed)

emptyDir

100MB

Usually not needed at runtime

Rust

/tmp

Temp files (rarely needed)

emptyDir

100MB

Usually not needed at runtime

Legend: PersistentVolumeClaim (PVC) represents data that must survive pod restarts (databases, caches with persistence). emptyDir represents ephemeral storage, recreated when pod starts (temporary files, non-persistent caches). tmpfs represents an in-memory mount (very fast, lost on restart, limited by available memory).

Per-Application Complete Examples

PostgreSQL

PostgreSQL requires two critical writable paths: /var/lib/postgresql/data (database files) and /var/run/postgresql (socket/runtime).

Dockerfile with Read-Only Setup

FROM postgres:16-alpine # Create necessary directories during buildRUN mkdir -p /var/lib/postgresql/data && \    mkdir -p /var/run/postgresql && \    chmod 700 /var/lib/postgresql/data && \    chown postgres:postgres /var/lib/postgresql/data /var/run/postgresql # Application runs as non-rootUSER postgres # CRITICAL: Use exec form (not shell form)ENTRYPOINT ["docker-entrypoint.sh"]CMD ["postgres"]

Kubernetes Manifest with Read-Only Filesystem

apiVersion: v1kind: Podmetadata:  name: postgres-db  namespace: databasespec:  securityContext:    readOnlyRootFilesystem: true    runAsNonRoot: true    runAsUser: 999  # postgres UID    fsGroup: 999   containers:  - name: postgres    image: postgres:16-alpine     # Security restrictions    securityContext:      allowPrivilegeEscalation: false      capabilities:        drop: ["ALL"]        add: ["NET_BIND_SERVICE"]  # For TCP port      readOnlyRootFilesystem: true     # Writable mounts    volumeMounts:    - name: data      mountPath: /var/lib/postgresql/data    - name: postgres-run      mountPath: /var/run/postgresql    - name: tmp      mountPath: /tmp     ports:    - name: postgres      containerPort: 5432      protocol: TCP     livenessProbe:      exec:        command:        - /bin/sh        - -c        - pg_isready -U postgres      initialDelaySeconds: 30      periodSeconds: 10     readinessProbe:      exec:        command:        - /bin/sh        - -c        - pg_isready -U postgres      initialDelaySeconds: 5      periodSeconds: 5     resources:      requests:        memory: "256Mi"        cpu: "250m"      limits:        memory: "1Gi"        cpu: "1000m"   volumes:  # Data must be persistent  - name: data    persistentVolumeClaim:      claimName: postgres-data-pvc   # Runtime directory (ephemeral)  - name: postgres-run    emptyDir: {}   # Temporary files  - name: tmp    emptyDir:      sizeLimit: 1Gi

Docker Compose with Read-Only

version: '3.8' services:  postgres:    image: postgres:16-alpine    read_only: true  # Read-only root filesystem     # Writable mounts    tmpfs:      - /var/run/postgresql  # Runtime socket      - /tmp                 # Temporary files     volumes:      - postgres_data:/var/lib/postgresql/data  # Persistent data     environment:      POSTGRES_PASSWORD: secure_password      POSTGRES_DB: application     ports:      - "5432:5432" volumes:  postgres_data:    driver: local

What Breaks Without Writable Paths

Missing Path

Error

Impact

/var/lib/postgresql/data

FATAL: could not create shared memory segment

Database won't start, complete failure

/var/run/postgresql

FATAL: could not create socket for Unix domain socket

Cannot accept connections

/tmp

ERROR: could not write to temporary file

Large queries fail, sort operations fail

Debugging Read-Only Failures

When PostgreSQL fails on read-only filesystem, check the actual error using kubectl logs pod/postgres-db. The output might show "FATAL: could not create shared memory segment: Permission denied". Verify volume mounts in pod spec by running kubectl get pod postgres-db -o yaml | grep -A 20 volumeMounts:. Test connection with mounted socket using kubectl exec -it pod/postgres-db -- psql -h /var/run/postgresql -U postgres -d postgres. If socket path is wrong, you'll see "could not translate host name "/var/run/postgresql" to address", which means you need to ensure /var/run/postgresql is in volumeMounts.

# Check actual errorkubectl logs pod/postgres-db # Verify volume mounts in pod speckubectl get pod postgres-db -o yaml | grep -A 20 volumeMounts: # Test connection with mounted socketkubectl exec -it pod/postgres-db -- \  psql -h /var/run/postgresql -U postgres -d postgres # If socket path is wrong, solution: Ensure /var/run/postgresql is in volumeMounts

Redis

Redis is simpler than PostgreSQL but still needs writable paths for persistence and temporary operations.

Dockerfile with Read-Only Setup

FROM redis:7-alpine # Create data directoryRUN mkdir -p /data && \    chown redis:redis /data && \    chmod 700 /data USER redis # Use exec formENTRYPOINT ["redis-server"]CMD ["--dir", "/data", "--appendonly", "yes"]

Kubernetes Manifest with Read-Only Filesystem

apiVersion: v1kind: Podmetadata:  name: redis-cachespec:  securityContext:    readOnlyRootFilesystem: true    runAsNonRoot: true    runAsUser: 999  # redis UID   containers:  - name: redis    image: redis:7-alpine     securityContext:      allowPrivilegeEscalation: false      capabilities:        drop: ["ALL"]      readOnlyRootFilesystem: true     # Writable mounts    volumeMounts:    - name: data      mountPath: /data    - name: tmp      mountPath: /tmp     ports:    - name: redis      containerPort: 6379     livenessProbe:      tcpSocket:        port: 6379      initialDelaySeconds: 10      periodSeconds: 5     resources:      requests:        memory: "128Mi"        cpu: "100m"      limits:        memory: "512Mi"        cpu: "500m"   volumes:  - name: data    persistentVolumeClaim:      claimName: redis-data-pvc   - name: tmp    emptyDir:      sizeLimit: 100Mi

Docker Compose with Read-Only

version: '3.8' services:  redis:    image: redis:7-alpine    read_only: true     tmpfs:      - /tmp     volumes:      - redis_data:/data     ports:      - "6379:6379"     command: redis-server --dir /data --appendonly yes volumes:  redis_data:

What Breaks Without Writable Paths

Missing Path

Error

Impact

/data

FATAL: unable to open the database file

Persistence fails, no AOF logging

/tmp

FATAL: cannot allocate temporary memory

Memory operations fail

Kafka

Kafka is the most critical: /var/lib/kafka/data contains the actual message log. Using emptyDir will lose data.

Dockerfile with Read-Only Setup

FROM confluentinc/cp-kafka:7.5-ubi8 # Kafka user typically 'appuser'RUN mkdir -p /var/lib/kafka/data && \    mkdir -p /tmp && \    chown appuser:appuser /var/lib/kafka/data /tmp USER appuser # Kafka startup script handles shell wrapping safelyENTRYPOINT ["/etc/confluent/docker/run"]

Kubernetes Manifest with Read-Only Filesystem

apiVersion: v1kind: Podmetadata:  name: kafka-broker-0spec:  securityContext:    readOnlyRootFilesystem: true    runAsNonRoot: true    runAsUser: 1000   containers:  - name: kafka    image: confluentinc/cp-kafka:7.5-ubi8     securityContext:      allowPrivilegeEscalation: false      capabilities:        drop: ["ALL"]        add: ["NET_BIND_SERVICE"]      readOnlyRootFilesystem: true     # CRITICAL: Use PersistentVolumeClaim for data    volumeMounts:    - name: kafka-data      mountPath: /var/lib/kafka/data    - name: tmp      mountPath: /tmp    - name: var-run      mountPath: /var/run     env:    - name: KAFKA_DATA_DIR      value: /var/lib/kafka/data    - name: KAFKA_BROKER_ID      value: "0"     ports:    - name: plaintext      containerPort: 9092    - name: controller      containerPort: 9093     livenessProbe:      tcpSocket:        port: 9092      initialDelaySeconds: 30      periodSeconds: 10     resources:      requests:        memory: "1Gi"        cpu: "500m"      limits:        memory: "2Gi"        cpu: "1000m"   volumes:  # DATA LOSS WARNING: Never use emptyDir for Kafka data  - name: kafka-data    persistentVolumeClaim:      claimName: kafka-broker-0-pvc   # Temp files during log compaction  - name: tmp    emptyDir:      sizeLimit: 10Gi   # Runtime files  - name: var-run    emptyDir:      sizeLimit: 100Mi

Docker Compose with Read-Only

version: '3.8' services:  kafka:    image: confluentinc/cp-kafka:7.5-ubi8    read_only: true     tmpfs:      - /tmp      - /var/run     # Use named volume (persists data)    # NEVER use tmpfs for Kafka data    volumes:      - kafka_data:/var/lib/kafka/data     environment:      KAFKA_DATA_DIR: /var/lib/kafka/data      KAFKA_BROKER_ID: "0"     ports:      - "9092:9092" volumes:  kafka_data:    driver: local

Critical: What Breaks Without Writable Paths

Missing Path

Error

Impact

/var/lib/kafka/data

FATAL: Failed to create directory

DATA LOSS - all messages lost

Using emptyDir for data

Pod restart loses all messages

COMPLETE DATA LOSS

/tmp

java.io.IOException: No space

Log compaction fails, consumer lag builds

DATA LOSS SCENARIO: When the pod restarts, emptyDir volumes at /var/lib/kafka/data are wiped. Kafka then starts with an empty directory, causing all historical messages to be lost permanently. Consumers subsequently see only new messages after the restart.

Correct Setup:

volumes:- name: kafka-data  persistentVolumeClaim:    claimName: kafka-broker-0-pvc  # ✓ Data survives pod restart

Nginx

Nginx caches request bodies, stores temporary files, and needs a runtime directory.

Dockerfile with Read-Only Setup

FROM nginx:alpine # Create necessary directoriesRUN mkdir -p /var/cache/nginx && \    mkdir -p /var/run && \    chown -R nginx:nginx /var/cache/nginx /var/run && \    chmod 700 /var/cache/nginx USER nginx # Copy pre-built config (created during image build)COPY nginx.conf /etc/nginx/nginx.conf ENTRYPOINT ["nginx"]CMD ["-g", "daemon off;"]

Kubernetes Manifest with Read-Only Filesystem

apiVersion: v1kind: Podmetadata:  name: nginx-webspec:  securityContext:    readOnlyRootFilesystem: true    runAsNonRoot: true    runAsUser: 101  # nginx UID   containers:  - name: nginx    image: nginx:alpine     securityContext:      allowPrivilegeEscalation: false      capabilities:        drop: ["ALL"]        add: ["NET_BIND_SERVICE"]  # Needed for port 80      readOnlyRootFilesystem: true     volumeMounts:    - name: cache      mountPath: /var/cache/nginx    - name: var-run      mountPath: /var/run    - name: tmp      mountPath: /tmp    - name: config      mountPath: /etc/nginx      readOnly: true     ports:    - name: http      containerPort: 80    - name: https      containerPort: 443     livenessProbe:      httpGet:        path: /health        port: 80      initialDelaySeconds: 5      periodSeconds: 10     resources:      requests:        memory: "64Mi"        cpu: "100m"      limits:        memory: "256Mi"        cpu: "500m"   volumes:  - name: cache    emptyDir:      sizeLimit: 5Gi   - name: var-run    emptyDir:      sizeLimit: 10Mi   - name: tmp    emptyDir:      sizeLimit: 1Gi   - name: config    configMap:      name: nginx-config

Docker Compose with Read-Only

version: '3.8' services:  nginx:    image: nginx:alpine    read_only: true     tmpfs:      - /var/cache/nginx      - /var/run      - /tmp     volumes:      - ./nginx.conf:/etc/nginx/nginx.conf:ro     ports:      - "80:80"      - "443:443"

What Breaks Without Writable Paths

Missing Path

Error

Impact

/var/cache/nginx

connect() failed (13: Permission denied)

Caching disabled, proxying fails

/var/run

open() "/var/run/nginx.pid" failed

PID file cannot be written

/tmp

temp file creation failed

Request body buffering fails

Python

Python applications need /tmp for package installation, virtual environments, and runtime temporary files.

Dockerfile with Read-Only Setup

FROM python:3.11-slim WORKDIR /app # Install dependencies during build (not at runtime)COPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt # Create app userRUN useradd -m -u 1000 appuser COPY --chown=appuser:appuser . . USER appuser # Exec form (no shell)ENTRYPOINT ["python", "-u", "app.py"]

Kubernetes Manifest with Read-Only Filesystem

apiVersion: v1kind: Podmetadata:  name: python-appspec:  securityContext:    readOnlyRootFilesystem: true    runAsNonRoot: true    runAsUser: 1000   containers:  - name: app    image: python-app:1.0.0     securityContext:      allowPrivilegeEscalation: false      capabilities:        drop: ["ALL"]      readOnlyRootFilesystem: true     volumeMounts:    - name: tmp      mountPath: /tmp    - name: cache      mountPath: /home/appuser/.cache     env:    - name: PYTHONUNBUFFERED      value: "1"    - name: TMPDIR      value: /tmp     ports:    - name: http      containerPort: 8000     livenessProbe:      httpGet:        path: /health        port: 8000      initialDelaySeconds: 10      periodSeconds: 10     resources:      requests:        memory: "256Mi"        cpu: "250m"      limits:        memory: "512Mi"        cpu: "500m"   volumes:  - name: tmp    emptyDir:      sizeLimit: 1Gi   - name: cache    emptyDir:      sizeLimit: 500Mi

Docker Compose with Read-Only

version: '3.8' services:  app:    build: .    read_only: true     tmpfs:      - /tmp      - /home/appuser/.cache     environment:      PYTHONUNBUFFERED: "1"      TMPDIR: /tmp     ports:      - "8000:8000"

What Breaks Without Writable Paths

Missing Path

Error

Impact

/tmp

OSError: [Errno 30] Read-only file system

Cannot create temp files, crashes

/home/appuser/.cache

pip/poetry caching fails

Slower package lookups

Node.js

Node.js needs /tmp for NPM package installation, native module compilation, and runtime temp files.

Dockerfile with Read-Only Setup

FROM node:18-alpine WORKDIR /app # Install dependencies during buildCOPY package*.json ./RUN npm ci --only=production && npm cache clean --force # Create non-root userRUN addgroup -g 1000 appuser && \    adduser -D -u 1000 -G appuser appuser COPY --chown=appuser:appuser . . USER appuser # Exec form (no shell)ENTRYPOINT ["node", "server.js"]

Kubernetes Manifest with Read-Only Filesystem

apiVersion: v1kind: Podmetadata:  name: node-appspec:  securityContext:    readOnlyRootFilesystem: true    runAsNonRoot: true    runAsUser: 1000   containers:  - name: app    image: node-app:1.0.0     securityContext:      allowPrivilegeEscalation: false      capabilities:        drop: ["ALL"]      readOnlyRootFilesystem: true     volumeMounts:    - name: tmp      mountPath: /tmp    - name: npm-cache      mountPath: /home/appuser/.npm     env:    - name: NODE_ENV      value: "production"    - name: NODE_OPTIONS      value: "--max-old-space-size=256"     ports:    - name: http      containerPort: 3000     livenessProbe:      httpGet:        path: /health        port: 3000      initialDelaySeconds: 10      periodSeconds: 10     resources:      requests:        memory: "256Mi"        cpu: "250m"      limits:        memory: "512Mi"        cpu: "500m"   volumes:  - name: tmp    emptyDir:      sizeLimit: 2Gi   - name: npm-cache    emptyDir:      sizeLimit: 1Gi

Docker Compose with Read-Only

version: '3.8' services:  app:    build: .    read_only: true     tmpfs:      - /tmp      - /home/appuser/.npm     environment:      NODE_ENV: production      NODE_OPTIONS: "--max-old-space-size=256"     ports:      - "3000:3000"

What Breaks Without Writable Paths

Missing Path

Error

Impact

/tmp

EACCES: permission denied, open '/tmp/...'

Crashes on first request

/home/appuser/.npm

npm caching fails

Slower package resolution

Java

Java applications need /tmp for JVM temporary files, garbage collection overhead, and optional heap dumps.

Dockerfile with Read-Only Setup

FROM eclipse-temurin:17-jre-alpine WORKDIR /app # Copy pre-built JAR (built in separate stage)COPY --chown=nobody:nogroup app.jar /app.jar USER nobody # Exec form (no shell)ENTRYPOINT ["java", "-jar", "/app.jar"]

Kubernetes Manifest with Read-Only Filesystem

apiVersion: v1kind: Podmetadata:  name: java-appspec:  securityContext:    readOnlyRootFilesystem: true    runAsNonRoot: true    runAsUser: 65534  # nobody   containers:  - name: app    image: java-app:1.0.0     securityContext:      allowPrivilegeEscalation: false      capabilities:        drop: ["ALL"]      readOnlyRootFilesystem: true     volumeMounts:    - name: tmp      mountPath: /tmp    - name: java-cache      mountPath: /home/nobody/.cache     env:    - name: JAVA_OPTS      value: "-Djava.io.tmpdir=/tmp -Xmx512m -Xms256m"     ports:    - name: http      containerPort: 8080     livenessProbe:      httpGet:        path: /actuator/health        port: 8080      initialDelaySeconds: 30      periodSeconds: 10     resources:      requests:        memory: "512Mi"        cpu: "500m"      limits:        memory: "1Gi"        cpu: "1000m"   volumes:  - name: tmp    emptyDir:      sizeLimit: 2Gi   - name: java-cache    emptyDir:      sizeLimit: 500Mi

Docker Compose with Read-Only

version: '3.8' services:  app:    image: java-app:1.0.0    read_only: true     tmpfs:      - /tmp      - /home/nobody/.cache     environment:      JAVA_OPTS: "-Djava.io.tmpdir=/tmp -Xmx512m -Xms256m"     ports:      - "8080:8080"

What Breaks Without Writable Paths

Missing Path

Error

Impact

/tmp

java.io.IOException: Cannot create temp files

JVM cannot allocate temporary memory, crashes

Large /tmp

OutOfMemoryError during GC

GC cannot write temporary data

Troubleshooting Read-Only Issues

Common Error: "Read-only file system"

Symptom: Application crashes with EACCES: permission denied or similar

When the application starts, it attempts to write to a path. Since the filesystem is read-only, the write fails with "Error: EACCES: permission denied, open '/var/cache/app/data.json'", causing the application to terminate.

Solution: Find which path needs to be writable. Check the error from logs using kubectl logs pod/myapp. The output will show something like "ERROR: Could not write to /var/cache/app". Add that path to volumeMounts in the pod spec. Define the volume in the volumes section. Restart the pod with kubectl rollout restart deployment/myapp.

# 1. Check the error from logskubectl logs pod/myapp# Output: "ERROR: Could not write to /var/cache/app" # 2. Add that path to volumeMountsvolumeMounts:- name: app-cache  mountPath: /var/cache/app # 3. Define the volumevolumes:- name: app-cache  emptyDir: {} # 4. Restart podkubectl rollout restart deployment/myapp

Finding Unexpected Write Paths Using strace

When an application fails on read-only filesystem but you don't know why, you can use strace to trace file operations. Create a debug image with debugging tools. Run strace to capture all file operations. Filter results to see which paths fail with permission denied.

# Create -dev image with debugging toolsFROM myapp:latestRUN apt-get update && apt-get install -y strace # Run strace to capture all file operationsdocker run --read-only --tmpfs /tmp myapp-dev:dev \  strace -e open,openat,write -f ./app 2>&1 | grep "Permission denied" # Output will show which paths fail:# openat(AT_FDCWD, "/var/log/app.log", ...) = -1 EACCES (Permission denied)

This reveals every path that needs to be writable.

Debugging Multi-Stage Build Issues

If dependencies installed in build stage aren't available in runtime, you have a multi-stage build problem. The wrong approach would have dependencies lost in runtime stage when copying from builder. Instead of using COPY --from=builder /app /app which misses dependencies, copy the entire Python site-packages.

FROM python:3.11 AS builderRUN pip install --target /deps numpy pandas FROM python:3.11-slimCOPY --from=builder /deps /usr/local/lib/python3.11/site-packages

Verifying Volume Mounts

Check that declared volumes are actually mounted. View pod manifest using kubectl get pod myapp -o yaml | grep -A 20 volumeMounts:. Expected output should show:

volumeMounts:- name: tmp  mountPath: /tmp- name: cache  mountPath: /var/cache/app

If a path is missing from this list, add it.

Testing Read-Only Locally

Before deploying to Kubernetes, test locally. Run Docker with read-only root and test tmpfs mounts. If this works, Kubernetes will work. If this fails, fix the volumes before deploying.

# Docker: Run with read-only rootdocker run --read-only \  --tmpfs /tmp \  --tmpfs /var/run \  -v mydata:/data \  myapp:latest

Performance Considerations

Volume Type Impact on Read-Only Filesystem

Volume Type

Speed

Persistence

Best For

emptyDir

Fast (local storage)

Ephemeral

Temp files, caches that don't need to survive

tmpfs

Very fast (RAM)

Ephemeral

Small temp files, performance-critical

PersistentVolumeClaim

Depends (NFS, block storage)

Persistent

Databases, message logs, data that must survive

Rule: Use the fastest option that meets your durability requirements.

Sizing emptyDir Volumes

Specify sizeLimit to prevent runaway temporary files. This fails writes if /tmp exceeds 2GB.

volumes:- name: tmp  emptyDir:    sizeLimit: 2Gi  # Fail writes if /tmp exceeds 2GB

When sizeLimit is exceeded, the application writes to /tmp and the emptyDir reaches its 2GB limit, the write fails with "No space left on device", and the pod may be evicted by kubelet.

Choose size based on your application: Python/Node.js needs 1-5GB for package installations and build artifacts. Java needs 2-8GB for GC overhead and large allocations. Nginx needs 1-10GB for request buffering and caches. Kafka needs 10-100GB for log compaction temp files.

Summary: Read-Only Filesystem Best Practices

  1. Always declare writable paths explicitly: Don't assume what your application needs
  2. Use appropriate volume types: PersistentVolumeClaim for data, emptyDir for temp, tmpfs for speed
  3. Set volume size limits: Prevent runaway temporary files
  4. Test locally first: docker run --read-only before deploying to Kubernetes
  5. Monitor for permission errors: Watch logs for "Read-only file system" errors during rollout
  6. Document your volumes: Add comments explaining why each path is writable

Read-only filesystems are now a standard security practice. CleanStart applications are designed to work cleanly with them across all application types.