Error: Cannot find module @rollup/rollup-linux-x64-gnu means Rollup's native binary for the machine you are on was never installed. Rollup 4 ships its parser as a separate compiled package per platform, npm installs only the one that matches, and three things reliably stop the right one arriving: a package-lock.json written on another operating system by an npm version with a known optional-dependency bug, a node_modules folder copied from another machine (very often into a Docker image), or an install run with optional dependencies switched off. The fix that works in nearly every case is the one Rollup prints itself: delete both node_modules and package-lock.json, run npm install again, and commit the new lockfile. The rest of this post is about doing that without breaking anything else, and about making sure it does not come back on the next deploy.
What the error actually says#
The full message comes from Rollup's own loader, and it is unusually honest:
Error: Cannot find module @rollup/rollup-linux-x64-gnu. npm has a bug relatedto optional dependencies (https://github.com/npm/cli/issues/4828). Please try`npm i` again after removing both package-lock.json and node_modules directory.The package name at the start is the important part. It tells you which platform Rollup decided it is running on, and therefore which binary it went looking for. Rollup builds the name from three things: the operating system, the CPU architecture, and on Linux the C library.
| Package suffix | Where you see it |
|---|---|
linux-x64-gnu | Ordinary Linux servers, Debian and Ubuntu images, most CI runners |
linux-x64-musl | Alpine Linux, including node:*-alpine images |
linux-arm64-gnu | ARM servers, Docker on Apple Silicon, Raspberry Pi 64-bit |
linux-arm64-musl | Alpine on ARM |
darwin-arm64 / darwin-x64 | macOS on Apple Silicon / Intel |
win32-x64-msvc | Windows on x64 |
win32-arm64-msvc | Windows on ARM |
There are more - FreeBSD, RISC-V, s390x, 32-bit ARM and others - but the table covers almost every report you will find. The suffix is a diagnosis in itself. If your server is a normal x64 Linux box and the error names linux-x64-musl, you are running inside Alpine whether you meant to or not. If you are on Windows and the error names win32-x64-msvc, someone else's lockfile is in play.
On Linux, Rollup decides between gnu and musl at runtime by asking Node for its process report: if the report carries a glibc version, it is gnu, otherwise musl. You do not configure this, and you cannot override it with an environment variable.
Why Rollup needs a binary at all, and why Vite brings it in#
Rollup 3 was pure JavaScript. Rollup 4, released in October 2023, replaced its JavaScript parser with SWC's, compiled from Rust into a native Node add-on. That made parsing much faster and introduced a problem every native package has: one compiled file cannot run on every platform.
The pattern Rollup uses, shared by esbuild, SWC, Lightning CSS, Tailwind CSS 4's @tailwindcss/oxide and sharp's @img/* packages, is to publish one tiny package per platform and list all of them in the main package's optionalDependencies. Each platform package declares the operating system and CPU it supports (and, for the Linux builds, which C library) in its own package.json:
{ "name": "@rollup/rollup-linux-x64-gnu", "os": ["linux"], "cpu": ["x64"], "libc": ["glibc"], "main": "./rollup.linux-x64-gnu.node"}When npm installs rollup, it sees twenty-odd optional dependencies, checks each one's os and cpu against the current machine, quietly skips the ones that do not match, and installs the one that does. Optional means a failure or a mismatch is not an error. That is exactly what lets this work across platforms, and exactly why it fails silently when it goes wrong: npm never tells you it skipped the one package you needed.
Most people never install Rollup on purpose. It arrives with Vite. Vite 5, 6 and 7 all depend on Rollup 4 for production builds, so vite build is where the error appears, usually in a CI job or on a server, rarely on the laptop where the project was set up. Upgrading from Vite 4 to Vite 5 was the moment a lot of projects met it for the first time, because Vite 4 still used Rollup 3 and had no platform binaries of its own beyond esbuild's.
Vite 8 replaces Rollup with Rolldown, which uses the same mechanism under different names: @rolldown/binding-linux-x64-gnu and friends. If you are on Vite 8 and see a missing native binding error from Rolldown, everything in this post applies - only the package names change.
The npm bug behind most cases#
The issue Rollup links to, npm/cli#4828, was opened in April 2022 and describes this: when npm regenerates package-lock.json while a node_modules folder is already present, it builds the lockfile from what is installed rather than from the full dependency metadata. On a Mac that means the lockfile ends up listing @rollup/rollup-darwin-arm64 and none of the other platform packages, because they were never installed there.
That lockfile is then committed. On Linux, npm ci installs exactly what the lockfile lists - which does not include a Linux binary - and finishes without a warning. The first sign of trouble is the build.
The usual trigger is innocent: someone deletes only the lockfile to fix a merge conflict or a peer dependency warning, runs npm install with node_modules still in place, and commits the result. It also happens on Windows, which is how Linux-generated lockfiles break Windows developers in the other direction with win32-x64-msvc.
The state of the fix is messy. Reports on the issue say recent npm 11 releases no longer drop the other platforms for packages that declare all of them as optional dependencies, which is Rollup's case, and other reports describe the problem returning in later 11.x releases. Do not plan around a particular npm version fixing it. Plan around a lockfile you have checked.
Confirm the cause before you change anything#
Two commands tell you which of the three causes you have. First, on the machine that fails, ask npm whether the binary is installed:
$ npm ls @rollup/rollup-linux-x64-gnumy-app@1.0.0 /srv/app└── (empty)An empty result means npm never installed it anywhere in the tree. Then look at the lockfile itself and count the platform entries:
$ grep -c '"node_modules/@rollup/rollup-' package-lock.json1A healthy lockfile lists every platform Rollup publishes, so the count is around twenty depending on the Rollup version. A count of one or two is the npm bug: the lockfile was generated on one machine and only knows that machine's binary. To see which one it does know:
$ grep '"node_modules/@rollup/rollup-' package-lock.json "node_modules/@rollup/rollup-darwin-arm64": {If the lockfile is complete and the package is still missing, the lockfile is not your problem. Look at how node_modules got onto the machine, and at whether optional dependencies are switched off - the next sections cover both.
The fix: regenerate the lockfile properly#
On the machine where you normally change dependencies:
$ rm -rf node_modules package-lock.json$ npm install$ grep -c '"node_modules/@rollup/rollup-' package-lock.json$ git add package-lock.json$ git commit -m "fix: regenerate lockfile with all platform binaries"On Windows in PowerShell, the first line is Remove-Item -Recurse -Force node_modules, package-lock.json. Deleting node_modules is the part that matters. Deleting only the lockfile with node_modules still present is precisely what triggers the bug.
This fix has a cost, and it is worth being clear about. Without a lockfile, npm install re-resolves every version range in package.json against what is published today. A ^5.4.0 that was pinned at 5.4.2 can come back as 5.4.19. For most projects that is harmless, but it is not nothing: read the lockfile diff, run the tests and a build, and deploy it as a dependency update rather than as a one-line fix. If you need the update to be smaller, the next section has narrower options.
On the server, nothing changes except that it now gets a correct lockfile. Keep using npm ci there - it installs exactly what the lockfile says, which is now the right thing. npm ci vs npm install covers why the server should never be the machine that writes a lockfile. If you were tempted to switch the server to npm install to make the error go away, do not: with an existing broken lockfile it will often still skip the binary, and when it does not, it is because it rewrote your dependency tree on the server.
Narrower fixes when you cannot regenerate everything#
Sometimes a full re-resolve is not acceptable - a release branch, an audit freeze, a monorepo where one lockfile covers forty packages. There are two narrower options, both with trade-offs.
Declare the missing binary yourself. Add the platform package to optionalDependencies in your own package.json, at exactly the same version as the rollup in your lockfile:
{ "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.40.0" }}Then run npm install so the lockfile records it. Because it is optional, npm skips it on the Mac and installs it on Linux. The catch is the version: the binary has to match the Rollup release it belongs to, and the next time Vite pulls in a newer Rollup your pin is out of date. Treat it as a stopgap, and remove it once you have regenerated the lockfile properly.
Use the WebAssembly build of Rollup. Rollup publishes @rollup/wasm-node, the same bundler with the parser compiled to WebAssembly instead of native code. It has no platform packages at all, so this whole class of error disappears. Point every copy of rollup in the tree at it with an npm override:
{ "overrides": { "rollup": "npm:@rollup/wasm-node" }}Run npm install after adding it. The price is speed - WebAssembly parsing is noticeably slower than native, which matters on large builds and barely registers on small ones. It is a reasonable choice for platforms Rollup has no native build for, and for teams that would rather have a slower build than a fragile one.
node_modules copied between machines, and Docker#
The second cause has nothing to do with npm's bug. A node_modules folder installed on one platform contains that platform's binary and nothing else, so moving the folder to a different platform leaves the new machine without its binary. The lockfile can be perfect and this still happens.
The common ways it happens:
- Uploading the project folder with `node_modules` inside, by SFTP or as a zip. A folder installed on a Mac carries
darwin-arm64and nothing for Linux. - Committing `node_modules` to Git. It should be in
.gitignore, always - it is large, it is platform-specific, and it is rebuilt from the lockfile in seconds. - Docker's `COPY . .` after `npm ci`. This is the classic. The image installs the right Linux binary, and then the next line copies your laptop's
node_modulesover the top of it.
The Dockerfile pattern that avoids it copies only the manifest files first, installs inside the image, and copies the source afterwards:
FROM node:22-bookworm-slim AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run buildThat alone is not enough, because COPY . . still copies a local node_modules if one exists. A .dockerignore file next to the Dockerfile stops it:
node_modulesdist.gitIt also makes builds faster, since Docker no longer sends hundreds of megabytes of build context it was going to throw away.
Docker Compose development setups have their own version of this. A bind mount such as .:/app replaces the image's /app with your project folder, including your laptop's node_modules. The usual fix is an anonymous volume for the folder, which keeps the container's own copy:
services: web: build: . volumes: - .:/app - /app/node_modulesDocker on a VDS and Docker Compose for small stacks cover the wider patterns.
Alpine, musl and ARM#
Alpine-based images use musl instead of glibc, so Rollup asks for linux-x64-musl. With a complete lockfile and an install that runs inside the Alpine image, this just works: npm picks the musl build. It breaks when the install ran somewhere else - a node_modules built on Debian or on the CI runner and copied into an Alpine stage installs linux-x64-gnu, and Rollup inside Alpine will not load it. The rule is the same as for every other case: run npm ci in the same image, or at least the same base, as the one that runs the build.
If you have no strong reason to use Alpine, a Debian-based image such as node:22-bookworm-slim removes a whole category of native-module surprises, at the cost of a somewhat larger image.
ARM gets people through Apple Silicon. Docker on an M-series Mac builds linux/arm64 images by default, so the install inside the build pulls linux-arm64-gnu. That image will not run on an x64 server for many reasons beyond Rollup. Build for the target explicitly:
$ docker build --platform linux/amd64 -t my-app .A related failure looks similar but is not the npm bug. If the stack trace mentions something like GLIBC_2.xx not found, the binary was installed but the system's C library is too old to load it - typically an ancient build image. Upgrade the base image rather than reinstalling.
Do not switch optional dependencies off#
The third cause is self-inflicted. npm ci --omit=optional, the older --no-optional, omit=optional in an .npmrc, or NPM_CONFIG_OMIT=optional in a CI environment all tell npm to skip every optional dependency - including the Rollup binary, which is optional only in the sense that npm should pick one. Rollup then fails exactly as if the bug had struck.
These settings tend to arrive from an old tutorial about shrinking images, or from a workaround for some other package's broken install script. Check what npm is actually using:
$ npm config get omitIt should print nothing, or dev if you deliberately skip dev dependencies. optional in that output is the problem. Note the separate trap: Vite and Rollup are devDependencies, so npm ci --omit=dev followed by vite build fails too, with vite: not found rather than this error. Install everything, build, then prune with npm prune --omit=dev.
On a hosted app server#
On a container-based host the same rules apply, and the only real decision is where the build runs. On RE:NODE, Node.js app plans deploy from GitHub through the panel's Git deploy, and when your start command runs npm ci and the build, they run on the server - so a lockfile that lists every Rollup platform is the whole fix: commit it and deploy again. The panel console shows the install and build output unfiltered, so a missing binary shows up as the error above rather than as a silent crash - reading the console covers what to look for. Do not upload a local node_modules through the file manager or SFTP; let npm ci build it on the server.
If the server keeps restarting because the build fails inside the start command, fix the lockfile before restarting again. A watcher counts crash restarts: three in an hour puts a warning on the server page and opens a ticket, six suspends the server. Deploying a Node app from GitHub walks through the start command and the build-then-prune sequence. If your Vite project builds to plain static files, you do not need Node at runtime at all - build it in CI and serve the dist folder, which static site hosting covers.
FAQ#
Why does the build work on my laptop but not on the server?
Your laptop has the binary for its own platform, installed when you first ran npm install. The server needs a different one, and either the lockfile does not list it or something stopped npm installing it. The grep -c check above tells you which.
Will deleting node_modules on the server fix it?
Not if the lockfile is the problem. npm ci deletes node_modules anyway and reinstalls from the lockfile, so a lockfile without the Linux binary produces the same result every time. The lockfile has to be regenerated and committed.
Should I install @rollup/rollup-linux-x64-gnu as a normal dependency?
No. A normal dependency is not optional, so the install fails outright on every Mac and Windows machine with an EBADPLATFORM error. If you need a stopgap, put it in optionalDependencies at Rollup's exact version.
Does using pnpm or Yarn avoid this?
The specific lockfile bug is npm's, so switching package manager often makes it go away. Copying node_modules between platforms and disabling optional dependencies break every package manager equally, so the Docker and .dockerignore advice still applies.
Is the same thing behind esbuild and SWC platform errors?
Yes, the mechanism is identical: one optional package per platform. esbuild's message is different - it says you installed esbuild for another platform than the one you are using - but the fixes are the same: a complete lockfile, an install on the target platform, and no copied node_modules.


