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:
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.
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.
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_modulesThe 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/binCheck ownership before and after:
ls -la /usr/local/lib/ | grep node_modulesThis 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 ~/.configThese 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 ~/.bashrcNow 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/tscThis 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 | bashRestart your terminal, then install Node.js:
nvm install --lts
nvm use --ltsVerify 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.xWith 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-appFix 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 --forceFix /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: drwxrwxrwtIf /tmp has wrong permissions:
sudo chmod 1777 /tmpUse a custom temp directory if you cannot fix /tmp:
export TMPDIR="$HOME/tmp"
mkdir -p "$TMPDIR"
npm installYou 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 pathFix 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.jsWith 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.targetWhen 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.jsonIf 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 ciGitLab 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_modulesfrom 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:
- Open Windows Security > Virus & threat protection > Manage settings
- Scroll to Exclusions > Add or remove exclusions
- 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 -ForceAlso enable long path support in Git:
git config --system core.longpaths trueRunning 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 -10Temporarily 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 immediatelyPermanent 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 3000Diagnosing 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-statusFix 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.nodeIf 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-appThis 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.logIf a parent directory does not exist, create it with the correct ownership:
sudo mkdir -p /opt/myapp/logs
sudo chown nodeapp:nodeapp /opt/myapp/logsRead-only filesystem
If the filesystem itself is mounted read-only, no permission change will help:
mount | grep ' / '
# Look for 'ro' in the mount optionsThis 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 65535For persistent changes, add to /etc/security/limits.conf:
nodeapp soft nofile 65535
nodeapp hard nofile 65535npm’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 filterIn Kubernetes, check if the pod has a restrictive security context:
securityContext:
readOnlyRootFilesystem: true # This blocks writes everywhere except mounted volumesIf 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
Fix: EACCES permission denied when installing npm packages globally
How to fix 'Error: EACCES: permission denied, access /usr/local/lib/node_modules' when running npm install -g on macOS or Linux. Multiple solutions ranked by recommendation.
Fix: ENOSPC: System limit for number of file watchers reached
How to fix the ENOSPC file watchers error on Linux by increasing the inotify watch limit, configuring VS Code, optimizing watched files, and handling Docker/WSL edge cases.
Fix: npm ERR! code ELIFECYCLE (errno 1, Failed at script)
How to fix npm ERR! code ELIFECYCLE, npm ERR! errno 1, and npm ERR! Failed at the script errors. Covers reading the real error, node_modules corruption, node-gyp failures, wrong Node version, memory issues, postinstall failures, Windows-specific fixes, and more.
Fix: SSL certificate problem: unable to get local issuer certificate
How to fix 'SSL certificate problem: unable to get local issuer certificate', 'CERT_HAS_EXPIRED', 'ERR_CERT_AUTHORITY_INVALID', and 'self signed certificate in certificate chain' errors in Git, curl, Node.js, Python, Docker, and more. Covers CA certificates, corporate proxies, Let's Encrypt, certificate chains, and self-signed certs.