Fix tar EPERM: operation not permitted, futime
Reproduce the ownership mismatch behind the warning, see how a current npm install actually fails, and fix the layer that creates it.
TL;DR#
npm WARN tar EPERM: operation not permitted, futime comes from tar (via npm) trying to set
the modification time of a file it just extracted, on a file the current process does not own.
futimes() can only set an arbitrary timestamp — the one stored in the archive entry — if the
caller owns the file or is root; a non-root user with only write access gets EPERM. The fix is
not sudo or deleting node_modules: it is keeping the UID that owns the app directory, the
npm cache, and anything baked in during the Docker build the same UID that runs
npm install, in every layer, before and after a USER switch.
The error#
npm WARN tar EPERM: operation not permitted, futime
npm WARN tar EPERM: operation not permitted, futime
npm WARN tar EPERM: operation not permitted, futime
...
npm timing action:extract Completed in 276816ms
npm timing action:finalize Completed in 172msThis is from the original report
(2019, 30 votes, 28,867 views): a node:alpine image, npm install fine as root, but thousands
of this warning and a hung install as a non-root user.
Why it happens#
tar's unpacker writes each file's bytes, then tries to restore the timestamp from the archive
entry so the extracted tree matches what was packed. In npm's bundled tar package this is a
direct call to fs.futimesSync(fd, atime, mtime), with a fallback to fs.utimesSync() on the
path if the file descriptor call fails — confirmed in the current source:
if (entry.mtime && !this.noMtime) {
const atime = entry.atime || new Date()
const mtime = entry.mtime
try {
fs.futimesSync(fd, atime, mtime)
} catch (futimeser) {
try {
fs.utimesSync(String(entry.absolute), atime, mtime)
} catch {
er = futimeser
}
}
}Both calls try to set the file's mtime to an arbitrary value read from the tar header, not
to "now". POSIX only lets you do that if you are the file's owner or the root user; owning
write permission on the file is not enough — write access alone only ever lets you bump a
timestamp to the current time, never set an older or custom one. So when a non-root process
extracts into (or over) files it does not own, futimes returns EPERM, the fallback utimes
fails the same way, and tar's own error handler treats that as a non-fatal entry error by
default: it warns and continues rather
than aborting the extraction — which is exactly why the 2019 report saw thousands of WARN lines
instead of one clean failure.
The ownership mismatch itself has one usual cause in a Docker image: a layer ran as root
(the default user, and the default for RUN npm install before any USER line) and left files
owned by UID 0, then a later USER node line switches to a non-root UID without ever chown-ing
what root created. The Dockerfile reference
is explicit about the default: "COPY … Sets ownership of copied files. Without this flag, files
are created with UID and GID of 0" — so any COPY without --chown before a non-root USER
plants the mismatch.
Reproducing it today#
Reproduced on the official node:22-alpine image with its bundled npm 10.9.9, using Docker
29.9.2 on this machine, 6 November 2026.
Negative control — non-root user, ownership consistent everywhere: no error.
=== non-root user (node uid 1000), ownership correct everywhere, no bind mount, no external volume ===
added 7 packages, and audited 8 packages in 9s
EXIT=0Ownership mismatch — a layer installed dependencies as root, then USER node without
chown -Ring node_modules; installing one more package as node:
npm error code EACCES
npm error syscall mkdir
npm error path /home/node/app/node_modules/is-even
npm error Error: EACCES: permission denied, mkdir '/home/node/app/node_modules/is-even'Downgrading an already-extracted package (so tar/npm overwrites files inside an existing
root-owned directory instead of creating a new one) fails the same way, one step later:
npm error code EACCES
npm error syscall rename
npm error path /home/node/app/node_modules/chalk
npm error dest /home/node/app/node_modules/.chalk-uXy2BkOP
npm error Error: EACCES: permission denied, rename '/home/node/app/node_modules/chalk' -> '/home/node/app/node_modules/.chalk-uXy2BkOP'Same root cause as the 2019 report — a non-root process meeting root-owned files it must
modify — but a different shape. Current npm extracts into its cache first and then moves files
into node_modules with its own mkdir/rename code (@npmcli/fs), not through tar's
warn-and-continue unpacking. The ownership problem now surfaces as one immediate, fatal EACCES
instead of a flood of tar WARN lines followed by a hang. The underlying permission issue is
identical; the failure mode is not.
A Docker-specific trap reproduces the same family: a freshly created named volume
(docker volume create, or an anonymous volume from -v name:/path with no prior content) is
owned by root:root until something inside the container chowns it. Mounting one as the npm
cache for a non-root user gives:
npm error code EACCES
npm error
npm error Your cache folder contains root-owned files, due to a bug in
npm error previous versions of npm which has since been addressed.
npm error
npm error To permanently fix this problem, please run:
npm error sudo chown -R 1000:1000 "/home/node/.npm"What no longer reproduces. The WSL1/old-Docker-Desktop angle from two of the original
answers — bind-mounting a Windows host folder and losing permission metadata — does not trigger
the error on current Docker Desktop with WSL2. Bind-mounting a Windows folder into
node:22-alpine and running npm install as the non-root node user installs cleanly; ls -la
inside the container shows the mounted files as rwxrwxrwx regardless of their real Windows
ownership, because Docker Desktop's current host-to-container file sharing grants full access
rather than enforcing UID-based permissions on that path.
Full raw output for every run above is in repro.log alongside this draft.
Fix#
The fix is ownership consistency, not disabling permission checks.
- Chown what root created, before switching user, in the same or a later
RUNlayer:
FROM node:22-alpine
WORKDIR /home/node/app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
RUN chown -R node:node /home/node/app
USER node- Or copy with the right owner from the start, so root never creates anything under the app directory:
FROM node:22-alpine
WORKDIR /home/node/app
COPY --chown=node:node package.json package-lock.json ./
USER node
RUN npm ci --omit=dev- If you mount the npm cache as a volume for a non-root user, chown it once:
docker run --rm -v npm-cache:/home/node/.npm node:22-alpine \
chown -R node:node /home/node/.npmor mount it under a path the Dockerfile already chowns, so it inherits the right owner
the first time a non-root npm install writes to it.
- Never mix a root-run install step with a later non-root install step on the same tree
without a
chown -Rbetween them — that is the one condition that reliably reproduces the error today, with or without Alpine.
sudo chown -R $(whoami) * run from outside the image, on the host, does nothing for files
that only exist inside a container's layers; it only helps when the mismatch is on a bind-mounted
host path, which is a different case from the one in this error.
Verify the fix#
docker build -t app .
docker run --rm app sh -c "whoami && npm install --dry-run"No tar WARN lines, no EACCES on mkdir or rename, and whoami should report the
non-root user the Dockerfile sets with USER.
If the ownership is now consistent but you instead see npm ERR! code EACCES pointing at
package-lock.json on a bind-mounted project directory (not inside the image), that is a
narrower case covered in Fix npm EACCES: permission denied on package-lock.json —
check that page's path against yours before assuming it is the same mismatch. A floating npm
self-upgrade failing on an unchanged Dockerfile is a different error family, covered in
Fix Docker npm EBADENGINE: Unsupported engine.
Outside Docker, pnpm can fail a rename mid-install on Windows for an unrelated reason: see
Fix pnpm ERR_PNPM_LINKING_FAILED on Windows —
there it is an open file handle, not UID ownership.
Ownership mismatches like this one are a recurring source of confusing, non-deterministic Docker failures precisely because the symptom (a warning flood, a hang, a clean fatal error) depends on which tool version is doing the extracting and moving — the underlying rule, that only an owner or root can rewrite a file's history, has not changed since the original 2019 report; only the code path that trips over it has.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix Node Sass could not find a binding for your environment
Read the two environment lines in the error, rebuild the binding for the Node that actually runs your build, and know when the only real fix is moving to Dart Sass.
Fix Docker npm EBADENGINE: Unsupported engine
Read the Required and Actual lines, distinguish npm self-upgrades from application installs, and stop a floating npm release breaking an unchanged Dockerfile.
Fix esbuild spawnSync ETXTBSY in Docker: Node 20.3 io_uring
Why npm install of esbuild started failing with ETXTBSY on node:20.3 images in June 2023, what changed in libuv and Docker since, and the build fix that holds today.


