This error occurs when Node.js attempts to write to a file or directory that resides on a read-only filesystem. It commonly happens in Docker containers with read-only root filesystems, mounted volumes with incorrect permissions, or filesystems explicitly mounted as read-only for security reasons.
The EACCES error with read-only filesystem indicates that Node.js is trying to perform a write operation (creating, modifying, or deleting files) on a filesystem that is mounted with read-only permissions. Unlike typical permission errors where changing file ownership or modes can help, this error stems from the underlying filesystem being immutable. This scenario is increasingly common in containerized environments where security best practices require read-only root filesystems (readOnlyRootFilesystem: true in Kubernetes) to prevent runtime tampering. Node.js applications that expect to write logs, cache files, temporary data, or node_modules will fail unless designated writable volumes are provided. The error can also occur with NFS mounts, container volumes mounted from Windows hosts, or any filesystem where the mount options include 'ro' (read-only). Understanding where your application writes data is essential for properly configuring writable paths in production environments.
Run your application locally with verbose logging to see all file write attempts:
# Enable Node.js filesystem debugging
NODE_DEBUG=fs node app.js
# Or add logging to your application
const fs = require('fs');
const originalWriteFile = fs.writeFile;
fs.writeFile = function(...args) {
console.log('Write attempt to:', args[0]);
return originalWriteFile.apply(this, args);
};Common directories Node.js applications write to:
- /tmp - temporary files
- /app/.cache - build and runtime caches
- /app/logs - application logs
- /root/.pm2 or /home/user/.pm2 - PM2 metadata
- /app/node_modules - if npm install runs at runtime
- /app/.next - Next.js build output
Document all write paths before proceeding with fixes.
In Docker or Kubernetes, mount writable volumes for directories that need write access:
Docker Compose:
services:
app:
image: myapp:latest
security_opt:
- no-new-privileges:true
read_only: true
volumes:
- /tmp
- ./logs:/app/logs
- cache-volume:/app/.cache
environment:
- NODE_ENV=production
volumes:
cache-volume:Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/.cache
- name: logs
mountPath: /app/logs
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
- name: logs
emptyDir: {}Use emptyDir for ephemeral storage or persistentVolumeClaim for persistent data.
Redirect cache and temporary directories to writable locations using environment variables:
# Dockerfile
FROM node:20-alpine
# Set writable directories for various tools
ENV NODE_OPTIONS="--max-old-space-size=2048"
ENV NPM_CONFIG_CACHE=/tmp/.npm
ENV PM2_HOME=/tmp/.pm2
ENV XDG_CACHE_HOME=/tmp/.cache
ENV TMPDIR=/tmp
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Ensure /tmp is writable at runtime
VOLUME /tmp
USER node
CMD ["node", "server.js"]For Next.js applications, ensure build happens during image creation (not at runtime):
RUN npm run build # Build .next during image creationMount /tmp as tmpfs for in-memory temporary storage:
Docker run:
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=100m \
myapp:latestDocker Compose:
services:
app:
image: myapp:latest
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=100m
- /run:rw,noexec,nosuid,size=10mKubernetes:
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 100MiThis provides a writable location without compromising security by keeping it in-memory.
Modify your application to gracefully handle read-only filesystems:
const fs = require('fs');
const path = require('path');
// Helper to find writable directory
function getWritableDir() {
const candidates = [
process.env.TMPDIR,
'/tmp',
process.env.HOME,
process.cwd()
];
for (const dir of candidates) {
if (dir) {
try {
const testFile = path.join(dir, '.write-test');
fs.writeFileSync(testFile, 'test');
fs.unlinkSync(testFile);
return dir;
} catch (err) {
// Directory not writable, try next
}
}
}
throw new Error('No writable directory found');
}
const cacheDir = path.join(getWritableDir(), 'app-cache');
// Ensure cache directory exists
try {
fs.mkdirSync(cacheDir, { recursive: true });
} catch (err) {
if (err.code !== 'EEXIST' && err.code !== 'EROFS') {
throw err;
}
console.warn('Cache directory unavailable, running without cache');
}This makes your application more resilient to different filesystem configurations.
PM2 in Read-Only Containers
PM2 requires a writable directory for its metadata. Set the PM2_HOME environment variable:
ENV PM2_HOME=/tmp/.pm2Or create a dedicated volume:
volumeMounts:
- name: pm2-data
mountPath: /app/.pm2
volumes:
- name: pm2-data
emptyDir: {}npm install in Production
Avoid running npm install at container startup. Instead, install dependencies during image build:
# Good: Install during build
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Bad: Install at runtime (requires writable filesystem)
# CMD npm install && node server.jsIf runtime installation is unavoidable, mount node_modules as a writable volume.
Next.js Build Output
Next.js requires a writable .next directory at runtime for server-side rendering. Two solutions:
1. Build at image creation time (recommended):
RUN npm run build
# .next is baked into image, no runtime writes needed2. Mount .next as writable volume (for dynamic builds):
volumeMounts:
- name: nextjs-build
mountPath: /app/.nextSecurity Considerations
Read-only root filesystems are a security best practice that:
- Prevent malware or compromised dependencies from modifying system files
- Reduce attack surface for privilege escalation
- Ensure reproducible deployments
When adding writable volumes:
- Limit sizes (use sizeLimit in emptyDir)
- Use noexec flag for tmpfs to prevent execution
- Never make system directories (/bin, /usr, /lib) writable
- Consider using subPath in volume mounts for finer control
NFS and Network Filesystems
For NFS mounts with permission errors:
# Check mount options
mount | grep nfs
# Remount with write permissions (requires root)
sudo mount -o remount,rw /mnt/nfs-share
# Or update /etc/fstab
nfs-server:/export /mnt/nfs-share nfs rw,sync,hard 0 0In containers, ensure the user ID inside matches the NFS export permissions.
Debugging Read-Only Issues
Check if filesystem is read-only:
# Inside container
mount | grep "ro,"
# Test write capability
touch /tmp/test-file && rm /tmp/test-file || echo "Write failed"
# Check specific directory
stat -f -c %T /path/to/dirProduction Monitoring
Add health checks that verify writable directories:
app.get('/healthz', (req, res) => {
const checks = {
tmp: canWrite('/tmp'),
cache: canWrite('/app/.cache'),
logs: canWrite('/app/logs')
};
const healthy = Object.values(checks).every(v => v);
res.status(healthy ? 200 : 503).json(checks);
});Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process