Error: EACCES: permission denied, open '/app/data/cache.json'

Node.js · updated 2026-11-03

Thrown by fs calls, by listen() on a port below 1024, and by npm installing global packages, when the operating system refuses the operation for the current user. The message names the syscall (open, mkdir, listen) and the path or port.

Error: EACCES: permission denied, open '/app/data/cache.json'
    at async open (node:internal/fs/promises:639:25)
    at async Object.writeFile (node:internal/fs/promises:1212:14)
    at async saveCache (file:///app/src/cache.js:18:3) {
  errno: -13,
  code: 'EACCES',
  syscall: 'open',
  path: '/app/data/cache.json'
}

Quick fix

Check which user the process runs as (id) and who owns the path (ls -l). Give that user ownership of the folders it writes, or write somewhere it owns. For ports below 1024, listen on 3000 or 8080 behind a reverse proxy instead of running Node as root.

  1. Run id (or whoami) as the app user and ls -ld on the path and its parent folder.
  2. In Docker, files copied as root are not writable by a non-root USER: chown them in the Dockerfile.
  3. Write runtime data to a dedicated folder the app user owns, not into the code directory.
  4. For "listen EACCES 0.0.0.0:80", use a high port behind nginx or a load balancer.
  5. For npm global installs, use a Node version manager or a user-owned prefix instead of sudo npm.
bash
# Who am I, and who owns the target?
id
ls -ld /app/data /app/data/cache.json

# Dockerfile: give the runtime user its data folder
#   RUN mkdir -p /app/data && chown -R node:node /app/data
#   USER node

# Port 80 needs root: listen high and proxy instead
PORT=3000 node server.js

Why it happens

The container runs as a non-root user

COPY in a Dockerfile creates files owned by root. Once USER node (or similar) takes over, writes to those folders fail with EACCES.

Writing into the code directory

Deploys often make the app folder read-only or owned by a deploy user, so caches, uploads and logs written next to the code fail in production only.

Binding a privileged port

Ports below 1024 need root (or a capability) on Linux, so listen(80) or listen(443) as a normal user raises listen EACCES.

Files created earlier by root

Running the app or a script once with sudo leaves root-owned files behind; the normal user then cannot change them, which looks like a sudden regression.

Other fixes

  • Mount volumes with the right ownership, or fix it in an entrypoint script before dropping privileges.
  • On macOS, a path under a protected folder may need the terminal or app to be granted access in Privacy settings.

Same error, other wording

  • Error: listen EACCES: permission denied 0.0.0.0:80
  • Error: EACCES: permission denied, mkdir '/app/uploads'
  • npm ERR! Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules'

Let your coding agent catch the next one

Runheed records this error in production with the stack trace, the breadcrumbs before it and the agent session that shipped the code, and serves it to Claude Code, Cursor or any MCP client. With it connected, you can ask:

> Find the EACCES errors in production, show me the syscall, path or port and which user the process runs as, and change the deploy (ownership, data folder or port) so the app never needs root.

Plans from $29/moSee what your agent readsSee the setupCancel any time from the billing portal

Related errors

References: Node.js errors: common system errors (EACCES), npm docs: resolving EACCES permissions errors