How to fix Error: EACCES: permission denied, write 'readonly' in Node.js

Node.jsINTERMEDIATEHIGHUpdated Dec 9, 2025ยท 195 views

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.

Quick fix

Fixes EACCES

# 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);
};
  1. Identify which directories your application needs to write to
  2. Add writable volume mounts for required directories
  3. Configure environment variables for writable paths
  4. Use tmpfs for temporary file operations
node.js โ€” error
# 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);
};
Error: EACCES: permission denied, write 'readonly'

What this error means

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.

Typical symptoms

  • Error: EACCES: permission denied, mkdir or write operations fail
  • EROFS: read-only file system errors in container logs
  • npm install fails with permission denied in mounted folders
  • Application crashes when attempting to create log files or cache directories
  • PM2 fails with "cannot create .pm2 directory" in Docker containers
  • Next.js build fails with "read-only file system, open '.next/package.json'"
  • File write operations succeed locally but fail in production containers

Common causes

  1. Docker container running with read-only root filesystem (securityContext.readOnlyRootFilesystem: true)
  2. Application tries to write to /app, /usr, or other protected directories in containers
  3. Node.js process attempts to create node_modules in a read-only mounted volume
  4. npm or package managers try to write cache files to immutable directories
  5. Logging libraries attempt to write to /var/log without a writable volume mount
  6. Temporary file operations target /tmp which is part of a read-only filesystem
  7. NFS or network filesystem mounted with read-only options
  8. Application runs in a minimal container image with filesystem mounted as read-only

How to fix "Error: EACCES: permission denied, write 'readonly'"

1Identify which directories your application needs to write to

Run your application locally with verbose logging to see all file write attempts:

bash
# 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.

2Add writable volume mounts for required directories

In Docker or Kubernetes, mount writable volumes for directories that need write access:

Docker Compose:

yaml
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:

yaml
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.

3Configure environment variables for writable paths

Redirect cache and temporary directories to writable locations using environment variables:

dockerfile
# 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):

dockerfile
RUN npm run build  # Build .next during image creation
4Use tmpfs for temporary file operations

Mount /tmp as tmpfs for in-memory temporary storage:

Docker run:

bash
docker run --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=100m \
  myapp:latest

Docker Compose:

yaml
services:
  app:
    image: myapp:latest
    read_only: true
    tmpfs:
      - /tmp:rw,noexec,nosuid,size=100m
      - /run:rw,noexec,nosuid,size=10m

Kubernetes:

yaml
volumeMounts:
- name: tmp
  mountPath: /tmp
volumes:
- name: tmp
  emptyDir:
    medium: Memory
    sizeLimit: 100Mi

This provides a writable location without compromising security by keeping it in-memory.

5Adjust application code to handle read-only environments

Modify your application to gracefully handle read-only filesystems:

javascript
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.

Advanced notes

PM2 in Read-Only Containers

PM2 requires a writable directory for its metadata. Set the PM2_HOME environment variable:

dockerfile
ENV PM2_HOME=/tmp/.pm2

Or create a dedicated volume:

yaml
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:

dockerfile
# 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.js

If 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):

dockerfile
RUN npm run build
# .next is baked into image, no runtime writes needed

2. Mount .next as writable volume (for dynamic builds):

yaml
volumeMounts:
- name: nextjs-build
  mountPath: /app/.next

Security 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:

bash
# 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 0

In containers, ensure the user ID inside matches the NFS export permissions.

Debugging Read-Only Issues

Check if filesystem is read-only:

bash
# 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/dir

Production Monitoring

Add health checks that verify writable directories:

javascript
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);
});

Frequently asked questions

What does "EACCES" mean?
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.
How do I fix "EACCES"?
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.
What causes "EACCES"?
Docker container running with read-only root filesystem (securityContext.readOnlyRootFilesystem: true) Application tries to write to /app, /usr, or other protected directories in containers Node.js process attempts to create node_modules in a read-only mounted volume
Is the "EACCES" error serious?
Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.

Official resources & further reading