01Why Use Read-Only Containers?
Read-only containers prevent attackers from:
• Installing malware or backdoors
• Modifying application binaries
• Persisting changes after restart
• Writing shell scripts for later execution
**Immutable infrastructure principle:**
Containers should be identical to their images. Any changes indicate compromise or misconfiguration.
**Compliance benefits:**
Many security frameworks require immutable workloads.
Read-only containers are a key defense-in-depth measure. Even if an attacker gains code execution, they can't modify the filesystem.
02Basic Read-Only Configuration
Set read_only: true in your Compose file. The entire container filesystem becomes read-only.
1services: 2 app: 3 image: myapp:latest4 read_only: true5 # Container cannot write to its filesystem67 nginx: 8 image: nginx:alpine9 read_only: true10 # Will fail! nginx needs to write to /var/cache/nginx1112 # Most apps need some writable paths...Most applications need some writable directories. Pure read_only: true usually fails without tmpfs mounts.
03Adding tmpfs for Runtime Data
Use tmpfs mounts for directories that need to be writable. tmpfs is stored in memory and doesn't persist across restarts.
**Common directories that need tmpfs:**
• /tmp - temporary files
• /var/run - PID files, sockets
• /var/cache - application caches
• /var/log - logs (if not using logging driver)
1services: 2 nginx: 3 image: nginx:alpine4 read_only: true5 tmpfs: 6 - /var/cache/nginx7 - /var/run8 - /tmp9 volumes: 10 - ./nginx.conf:/etc/nginx/nginx.conf:ro11 - ./html:/usr/share/nginx/html:ro1213 app: 14 image: node:alpine15 read_only: true16 tmpfs: 17 - /tmp18 - /app/.npm # npm cache19 - /home/node/.npm # user npm cache20 working_dir: /app21 volumes: 22 - ./app:/app:roStart with read_only: true and add tmpfs paths as you discover what the application needs to write.
04Discovering Required Write Paths
When enabling read-only mode, you'll need to find which paths your application writes to. Here's how to discover them.
1# Method 1: Run normally and check for write failures2docker compose up3# Look for "Read-only file system" errors in logs45# Method 2: Use strace to find write operations6docker run --rm -it --cap-add SYS_PTRACE myapp strace -f -e trace=write,open,openat -p 178# Method 3: Run with read_only and verbose error output9docker run --rm --read-only myapp 2>&1 | grep -i "read-only"1011# Method 4: Compare filesystem after running12docker run -d --name test myapp13sleep 1014docker diff test15# Shows files added (A), changed (C), or deleted (D)16docker rm -f test1718# Common paths that need tmpfs:19# /tmp, /var/tmp - temp files20# /var/run, /run - PID files, sockets21# /var/cache - caches22# /var/log - logs23# /dev/shm - shared memory05Complete Read-Only Examples
Here are ready read-only configurations for common services.
1services: 2 # PostgreSQL (read-only except data)3 postgres: 4 image: postgres:16-alpine5 read_only: true6 tmpfs: 7 - /tmp8 - /var/run/postgresql9 volumes: 10 - pgdata:/var/lib/postgresql/data11 environment: 12 - POSTGRES_PASSWORD=${DB_PASSWORD}1314 # Redis (read-only except data)15 redis: 16 image: redis:alpine17 read_only: true18 tmpfs: 19 - /tmp20 volumes: 21 - redis_data:/data22 command: redis-server --appendonly yes2324 # Traefik (fully read-only)25 traefik: 26 image: traefik:v3.027 read_only: true28 tmpfs: 29 - /tmp30 volumes: 31 - /var/run/docker.sock:/var/run/docker.sock:ro32 - ./traefik.yml:/etc/traefik/traefik.yml:ro33 - ./certs:/certs:ro3435volumes: 36 pgdata: 37 redis_data: 06Additional Hardening Options
Combine read_only with other security options for defense in depth.
1services: 2 hardened-app: 3 image: myapp:latest4 read_only: true56 # Drop all capabilities, add only what's needed7 cap_drop: 8 - ALL9 cap_add: 10 - NET_BIND_SERVICE # Only if binding to ports < 10241112 # Prevent privilege escalation13 security_opt: 14 - no-new-privileges:true1516 # Run as non-root17 user: "1000:1000"1819 # Limit resources20 deploy: 21 resources: 22 limits: 23 memory: 256M24 cpus: '0.5'2526 # tmpfs for runtime needs27 tmpfs: 28 - /tmp:size=64M,mode=17772930 # All mounts read-only31 volumes: 32 - ./config:/app/config:ro33 - ./data:/app/data:roThe combination of read_only, cap_drop, no-new-privileges, and non-root user provides strong container isolation.