Fix: EACCES: permission denied, mkdir / open / unlink (Node.js)

The Error

You run a Node.js application or an npm command and get:

Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/my-package'

Or one of these variations:

Error: EACCES: permission denied, open '/var/log/app.log'
Error: EACCES: permission denied, unlink '/tmp/cache/session-abc123'
Error: EACCES: permission denied, scandir '/root/.npm/_cacache'
Error: EACCES: permission denied, access '/usr/local/lib/node_modules'
Error: EACCES: permission denied, mkdir '/home/app/.cache/node'
npm ERR! Error: EACCES: permission denied, rename '/usr/local/lib/node_modules/.package-lock.json'

The EACCES error code comes from the POSIX standard. It means the Node.js process attempted a filesystem operation — creating a directory, reading a file, deleting a file, or listing directory contents — and the operating system denied it because the process does not have the required permission on that path.

Why This Happens

Node.js delegates all filesystem operations to the operating system kernel through libuv. When your code calls fs.mkdirSync('/some/path') or fs.writeFileSync('/some/file', data), Node.js asks the OS to perform that operation on behalf of the user running the process. The OS checks three things:

  1. File ownership. Every file and directory has an owner (user) and a group. The OS checks whether the running process’s effective user ID matches the file’s owner, the file’s group, or falls into the “others” category.

  2. Permission bits. Each of the three categories (owner, group, others) has separate read (r), write (w), and execute (x) bits. To create a file in a directory, you need write and execute permission on that directory. To read a file, you need read permission. To delete (unlink) a file, you need write permission on the directory containing it.

  3. Security modules. Even when standard Unix permissions allow the operation, mandatory access control systems like SELinux or AppArmor can independently deny it.

The most common scenario that triggers EACCES in Node.js is trying to write to a directory owned by root while running as a non-root user. This happens frequently with npm global installs, cache directories, and log files. You can check the ownership and permissions of any path with:

ls -la /usr/local/lib/node_modules/

Output like this reveals the problem:

drwxr-xr-x 5 root root 4096 Apr 10 09:00 node_modules

The directory is owned by root, and only the owner has write permission. Your non-root user can read and enter the directory (r-x for others) but cannot create files or subdirectories in it.

Fix 1: Fix Directory Ownership with chown

The most direct fix is to change ownership of the directory to your user:

sudo chown -R $USER:$USER /usr/local/lib/node_modules
sudo chown -R $USER:$USER /usr/local/bin

Check ownership before and after:

ls -la /usr/local/lib/ | grep node_modules

This approach works well for development machines where you are the only user. For shared servers or production systems, the other fixes below are more appropriate because changing ownership of system directories affects all users.

If the error is about a directory inside your home folder (like ~/.npm or ~/.config), this is almost always the right fix:

sudo chown -R $USER:$USER ~/.npm
sudo chown -R $USER:$USER ~/.config

These directories should never be owned by root. They usually end up root-owned because someone ran sudo npm install at some point, which created cache files as root inside your home directory. For a deeper walkthrough of npm global permission errors, see Fix: EACCES permission denied when installing npm packages globally.

Fix 2: Configure npm Global Prefix to a User-Owned Directory

Instead of changing ownership of system directories, you can tell npm to install global packages in a directory you already own:

mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'

Then add the new bin directory to your PATH. Add this line to ~/.bashrc, ~/.zshrc, or ~/.profile:

export PATH="$HOME/.npm-global/bin:$PATH"

Reload your shell:

source ~/.bashrc

Now npm install -g writes to ~/.npm-global instead of /usr/local, and you never need sudo for global installs:

npm install -g typescript
which tsc
# /home/youruser/.npm-global/bin/tsc

This is npm’s officially recommended solution for EACCES errors on global installs. It works on any Linux distribution and macOS without requiring elevated privileges.

Fix 3: Use nvm to Avoid Permission Issues Entirely

Node Version Manager (nvm) installs Node.js and npm in your home directory, which means every operation — including global package installs — runs in user-owned space. No sudo, no permission issues.

Install nvm:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

Restart your terminal, then install Node.js:

nvm install --lts
nvm use --lts

Verify the installation paths are in your home directory:

which node
# /home/youruser/.nvm/versions/node/v22.x.x/bin/node

which npm
# /home/youruser/.nvm/versions/node/v22.x.x/bin/npm

npm config get prefix
# /home/youruser/.nvm/versions/node/v22.x.x

With nvm, npm install -g writes to ~/.nvm/versions/node/<version>/lib/node_modules/, which your user owns. If you previously installed Node.js through your system package manager or from the official website, uninstall that version first to avoid conflicts.

If you are running into module resolution errors after switching to nvm, see Fix: Cannot find module (Node.js / TypeScript).

Fix 4: Docker USER Directive and Volume Permissions

When running Node.js inside Docker, EACCES errors are extremely common. The default Docker user is root, but many Node.js base images switch to a non-root user, or you intentionally run as non-root for security. The problem arises when the container user does not own the directories it needs to write to.

Fix in Dockerfile — create and own the app directory:

FROM node:22-alpine

# The node image includes a 'node' user (uid 1000)
# Create app directory and set ownership before switching user
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app

WORKDIR /home/node/app

# Copy package files as root, then fix ownership
COPY --chown=node:node package*.json ./

USER node

RUN npm ci

COPY --chown=node:node . .

CMD ["node", "server.js"]

Fix for mounted volumes at runtime:

docker run -u $(id -u):$(id -g) -v $(pwd):/app my-node-app

Fix in Docker Compose:

services:
  app:
    build: .
    user: "1000:1000"
    volumes:
      - ./src:/app/src
      - node_modules:/app/node_modules
volumes:
  node_modules:

Using a named volume for node_modules avoids permission conflicts between the host and container filesystem. The container owns the named volume entirely.

A common mistake is running npm install as root in the Dockerfile and then switching to a non-root user. The node_modules directory ends up owned by root, and the non-root user cannot update or remove packages. Always switch to the non-root user before running npm install. For more Docker permission troubleshooting, see Fix: Docker Permission Denied While Trying to Connect to the Docker Daemon Socket.

Fix 5: /tmp and Cache Directory Permissions

Node.js and npm use temporary and cache directories extensively. When multiple users or processes share the same system, these directories can end up with mixed ownership:

Error: EACCES: permission denied, mkdir '/tmp/npm-12345'
Error: EACCES: permission denied, open '/home/user/.npm/_cacache/tmp/abc123'

Fix the npm cache:

# Check current cache location
npm config get cache
# Usually: ~/.npm

# Fix ownership
sudo chown -R $USER:$USER $(npm config get cache)

# Or clear and rebuild the cache
npm cache clean --force

Fix /tmp permission issues:

The /tmp directory should have the sticky bit set (permissions 1777), which allows any user to create files but only the owner can delete them:

ls -ld /tmp
# Should show: drwxrwxrwt

If /tmp has wrong permissions:

sudo chmod 1777 /tmp

Use a custom temp directory if you cannot fix /tmp:

export TMPDIR="$HOME/tmp"
mkdir -p "$TMPDIR"
npm install

You can also set the temp directory in your Node.js application:

const os = require('os');
process.env.TMPDIR = '/path/to/writable/tmp';
// os.tmpdir() will now return your custom path

Fix 6: Running as Root vs Non-Root

Running Node.js as root eliminates EACCES errors but creates security risks and often causes more permission problems downstream. Here is when each approach makes sense:

Running as non-root (recommended for almost everything):

# Create a dedicated user for your application
sudo useradd -m -s /bin/bash nodeapp
sudo mkdir -p /var/log/myapp /var/lib/myapp
sudo chown -R nodeapp:nodeapp /var/log/myapp /var/lib/myapp

# Run the application as that user
sudo -u nodeapp node /opt/myapp/server.js

With systemd:

[Unit]
Description=My Node.js App
After=network.target

[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure

# Grant access to specific directories
ReadWritePaths=/var/log/myapp /var/lib/myapp

[Install]
WantedBy=multi-user.target

When root causes more problems: If you run npm install as root, it creates node_modules owned by root. Then your non-root application cannot write to that directory (for example, to update lock files or write to a local SQLite database). The fix is to consistently run everything as the same user. If you ran npm commands as root accidentally, fix the resulting ownership:

sudo chown -R $USER:$USER node_modules/
sudo chown -R $USER:$USER package-lock.json

If your npm lifecycle scripts are failing with exit codes during install, see Fix: npm ERR! code ELIFECYCLE for additional troubleshooting steps.

Fix 7: CI/CD Permission Issues

CI/CD environments (GitHub Actions, GitLab CI, Jenkins, CircleCI) frequently trigger EACCES errors because of how they manage workspaces, caches, and Docker containers.

GitHub Actions:

- name: Fix permissions
  run: sudo chown -R $USER:$USER $GITHUB_WORKSPACE

- name: Install dependencies
  run: npm ci

- name: Set npm cache directory
  run: |
    npm config set cache $GITHUB_WORKSPACE/.npm-cache
    npm ci

GitLab CI with Docker executor:

before_script:
  - npm config set cache .npm --userconfig .npmrc
  - chown -R $(id -u):$(id -g) .

install:
  script:
    - npm ci --cache .npm
  cache:
    paths:
      - .npm/

Jenkins:

Jenkins agents often run as the jenkins user, which may not own the workspace directory after certain operations:

pipeline {
    agent any
    stages {
        stage('Install') {
            steps {
                sh 'sudo chown -R jenkins:jenkins ${WORKSPACE}'
                sh 'npm ci'
            }
        }
    }
}

Common CI/CD causes of EACCES:

  • Cached node_modules from a previous build that ran as a different user (or root)
  • Docker-in-Docker setups where the inner container user doesn’t match the outer one
  • Volume mounts from the CI host into a container with mismatched UIDs
  • Build artifacts from a previous step that set restrictive permissions

The simplest fix in any CI/CD system is to ensure the user running npm ci or npm install owns the entire workspace directory before the install step runs.

Fix 8: Windows Permission Issues

On Windows, EACCES errors in Node.js have different root causes than on Linux and macOS, since Windows uses ACLs rather than Unix permission bits.

Antivirus software locking files:

Windows Defender and other antivirus programs scan files as they are created, which can temporarily lock them. When Node.js or npm tries to delete or rename a file that the antivirus is scanning, you get EACCES:

Error: EACCES: permission denied, unlink 'C:\Users\you\project\node_modules\.package-lock.json'

Fix: Add your project directory and Node.js installation directory to the antivirus exclusion list. For Windows Defender:

  1. Open Windows Security > Virus & threat protection > Manage settings
  2. Scroll to Exclusions > Add or remove exclusions
  3. Add your project folder and %APPDATA%\npm

File or directory in use by another process:

Windows does not allow deleting files that are open by any process. If npm install fails with EACCES on an unlink operation, close any editors, file explorers, or terminals that might have the directory open, then retry.

Long path names:

Windows has a 260-character path limit by default. Deeply nested node_modules directories can exceed this, causing EACCES or EPERM errors. Enable long paths:

# Run as Administrator
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force

Also enable long path support in Git:

git config --system core.longpaths true

Running without Administrator privileges:

Some npm packages with native addons (node-gyp) need write access to system directories. Use a terminal with Administrator privileges, or better yet, use nvm-windows to keep everything in user space.

For general bash permission errors on Linux and macOS, see Fix: bash: Permission denied.

Fix 9: SELinux and AppArmor Blocking Node.js

On systems with mandatory access control, you can have correct Unix permissions and still get EACCES. SELinux (Fedora, RHEL, CentOS, Amazon Linux) and AppArmor (Ubuntu, Debian) independently evaluate whether a process is allowed to perform a filesystem operation.

Diagnosing SELinux denials:

# Check if SELinux is enforcing
getenforce

# Search for recent denials related to node
sudo ausearch -m avc -ts recent | grep node

# Check the audit log
sudo grep "denied.*node" /var/log/audit/audit.log | tail -10

Temporarily disable SELinux to confirm it is the cause:

sudo setenforce 0
# Run your Node.js application again
# If it works, SELinux was the problem
sudo setenforce 1  # re-enable immediately

Permanent fix — set the correct SELinux context:

# Allow Node.js to read and write to your app directory
sudo semanage fcontext -a -t httpd_sys_rw_content_t "/opt/myapp(/.*)?"
sudo restorecon -Rv /opt/myapp

# If your app uses a non-standard port, allow it
sudo semanage port -a -t http_port_t -p tcp 3000

Diagnosing AppArmor denials:

# Check for AppArmor denials in system logs
sudo dmesg | grep -i apparmor | tail -10
sudo journalctl -k | grep -i apparmor | tail -10

# Check which profiles are enforcing
sudo aa-status

Fix AppArmor denials:

# Put the profile in complain mode (logs instead of blocking)
sudo aa-complain /usr/bin/node

# Or create a local override to allow specific paths
sudo nano /etc/apparmor.d/local/usr.bin.node
# Add: /opt/myapp/** rw,
sudo apparmor_parser -r /etc/apparmor.d/usr.bin.node

If your Node.js application runs in a Docker container on a SELinux-enabled host, add the :z flag to volume mounts:

docker run -v /opt/myapp/data:/app/data:z my-node-app

This tells Docker to relabel the volume contents with the correct SELinux context for the container.

Still Not Working?

The path does not exist

EACCES usually means a permission problem, but occasionally Node.js reports EACCES when the issue is actually a missing parent directory. Verify the full path exists:

# Check every component of the path
namei -l /opt/myapp/logs/output.log

If a parent directory does not exist, create it with the correct ownership:

sudo mkdir -p /opt/myapp/logs
sudo chown nodeapp:nodeapp /opt/myapp/logs

Read-only filesystem

If the filesystem itself is mounted read-only, no permission change will help:

mount | grep ' / '
# Look for 'ro' in the mount options

This is common in containerized environments, certain cloud instances, and when a disk has errors. If intentional (like a read-only Docker layer), write to a volume or tmpfs mount instead.

File handle or inode exhaustion

When the system runs out of file handles or inodes, the errors can sometimes appear as EACCES rather than EMFILE or ENOSPC:

# Check open file limits
ulimit -n

# Check inode usage
df -i

# Increase the open file limit for the current session
ulimit -n 65535

For persistent changes, add to /etc/security/limits.conf:

nodeapp  soft  nofile  65535
nodeapp  hard  nofile  65535

npm’s global package directory is corrupted

If fixing permissions does not resolve npm install -g errors, the global node_modules directory may have structural issues. In that case, remove and reinstall:

# Check the prefix
npm config get prefix

# Remove the global node_modules (be careful with this)
sudo rm -rf /usr/local/lib/node_modules
sudo mkdir /usr/local/lib/node_modules
sudo chown -R $USER:$USER /usr/local/lib/node_modules

# Reinstall global packages
npm install -g <your-packages>

Process is sandboxed

Some environments sandbox Node.js processes using namespaces, seccomp, or container runtimes. In these cases, the process may genuinely be restricted from accessing certain paths. Check if your process is running in a restricted environment:

cat /proc/self/status | grep Seccomp
# Seccomp: 2 means seccomp is active with a filter

In Kubernetes, check if the pod has a restrictive security context:

securityContext:
  readOnlyRootFilesystem: true  # This blocks writes everywhere except mounted volumes

If readOnlyRootFilesystem is true, you must write to an explicitly mounted volume or an emptyDir mount.


Related: Fix: EACCES permission denied when installing npm packages globally | Fix: bash: Permission denied | Fix: Docker Permission Denied While Trying to Connect to the Docker Daemon Socket | Fix: npm ERR! code ELIFECYCLE | Fix: Cannot find module (Node.js / TypeScript)

Related Articles