docker.recipes
Security7 min read

Read-Only Containers for Security

Prevent container filesystem modifications using read_only mode with strategic tmpfs mounts for runtime data.

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:latest
4 read_only: true
5 # Container cannot write to its filesystem
6
7 nginx:
8 image: nginx:alpine
9 read_only: true
10 # Will fail! nginx needs to write to /var/cache/nginx
11
12 # 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:alpine
4 read_only: true
5 tmpfs:
6 - /var/cache/nginx
7 - /var/run
8 - /tmp
9 volumes:
10 - ./nginx.conf:/etc/nginx/nginx.conf:ro
11 - ./html:/usr/share/nginx/html:ro
12
13 app:
14 image: node:alpine
15 read_only: true
16 tmpfs:
17 - /tmp
18 - /app/.npm # npm cache
19 - /home/node/.npm # user npm cache
20 working_dir: /app
21 volumes:
22 - ./app:/app:ro

Start 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 failures
2docker compose up
3# Look for "Read-only file system" errors in logs
4
5# Method 2: Use strace to find write operations
6docker run --rm -it --cap-add SYS_PTRACE myapp strace -f -e trace=write,open,openat -p 1
7
8# Method 3: Run with read_only and verbose error output
9docker run --rm --read-only myapp 2>&1 | grep -i "read-only"
10
11# Method 4: Compare filesystem after running
12docker run -d --name test myapp
13sleep 10
14docker diff test
15# Shows files added (A), changed (C), or deleted (D)
16docker rm -f test
17
18# Common paths that need tmpfs:
19# /tmp, /var/tmp - temp files
20# /var/run, /run - PID files, sockets
21# /var/cache - caches
22# /var/log - logs
23# /dev/shm - shared memory

05Complete 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-alpine
5 read_only: true
6 tmpfs:
7 - /tmp
8 - /var/run/postgresql
9 volumes:
10 - pgdata:/var/lib/postgresql/data
11 environment:
12 - POSTGRES_PASSWORD=${DB_PASSWORD}
13
14 # Redis (read-only except data)
15 redis:
16 image: redis:alpine
17 read_only: true
18 tmpfs:
19 - /tmp
20 volumes:
21 - redis_data:/data
22 command: redis-server --appendonly yes
23
24 # Traefik (fully read-only)
25 traefik:
26 image: traefik:v3.0
27 read_only: true
28 tmpfs:
29 - /tmp
30 volumes:
31 - /var/run/docker.sock:/var/run/docker.sock:ro
32 - ./traefik.yml:/etc/traefik/traefik.yml:ro
33 - ./certs:/certs:ro
34
35volumes:
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:latest
4 read_only: true
5
6 # Drop all capabilities, add only what's needed
7 cap_drop:
8 - ALL
9 cap_add:
10 - NET_BIND_SERVICE # Only if binding to ports < 1024
11
12 # Prevent privilege escalation
13 security_opt:
14 - no-new-privileges:true
15
16 # Run as non-root
17 user: "1000:1000"
18
19 # Limit resources
20 deploy:
21 resources:
22 limits:
23 memory: 256M
24 cpus: '0.5'
25
26 # tmpfs for runtime needs
27 tmpfs:
28 - /tmp:size=64M,mode=1777
29
30 # All mounts read-only
31 volumes:
32 - ./config:/app/config:ro
33 - ./data:/app/data:ro

The combination of read_only, cap_drop, no-new-privileges, and non-root user provides strong container isolation.