Next.js 'Found multiple lockfiles': a silent monorepo footgun, not just noise
Your Next.js build prints a yellow warning about multiple lockfiles, picks one, and carries on. The app runs locally. The build passes in CI. So you file the warning under "noise" and move on, the way everyone does. Then one deploy the standalone container boots and immediately dies with a module it swears does not exist, even though it is right there in your repo.
That warning was not noise. It was Next.js telling you it had to guess where your project really begins, and in a monorepo, or a Docker build, or an app scaffolded from a template, it can guess wrong. When it guesses wrong, it does not error. It quietly traces the wrong set of files into your production output, and you find out in production.
Next.js infers your workspace root by walking up the tree for a lockfile. Multiple lockfiles mean it has to pick one, and the pick sets the root for file tracing. A wrong pick silently drops files from output: 'standalone'.
- Do not ignore it. Pin the root in
next.config.js: setturbopack.rootto your real monorepo root to fix dev/build detection and silence the warning. - Set
outputFileTracingRootto the same path so standalone tracing starts from the right place. - Delete stray lockfiles from the Docker build context and from template-derived example apps, so inference has nothing wrong to pick.
- Verify by checking the standalone output actually contains the shared package files before you deploy it.
The exact warning
Warning: Found multiple lockfiles. Selecting /app/apps/web/package-lock.json because it is closest to the current directory.
Read the path it selected, not just the fact of the warning. In the example above it chose the lockfile inside the app (apps/web), when the real workspace root is a level or two up where the canonical lockfile lives. "Closest to the current directory" is the whole problem: closest is not the same as correct, and in a monorepo the correct one is usually further away, at the root.
Why this happens: the lockfile is how Next.js finds the edge of your project
Next.js does not have a declared "this is the project root" marker it trusts above all else. To know where your workspace begins, so it can trace which files a page needs and copy the right ones for a standalone deployment, it walks up from your app directory looking for a lockfile. The nearest lockfile is treated as the marker of the root.
In a clean single-app repo there is exactly one lockfile and this works invisibly. In the real world there are often two, and each extra one is a false marker:
Where do the extra lockfiles come from? Almost always one of three places. A template you scaffolded the app from shipped its own package-lock.json inside the app folder, and you never noticed. A Docker multi-stage build did a partial COPY that placed a lockfile at a level that does not match your real layout. Or a nested example app in the repo has its own lockfile that happens to sit between your app and the true root. In every case the file is closer than the real one, so it wins.
Two subsystems consume this inferred root, and both are affected. Turbopack uses a workspace root for dev and build, and modern Next.js exposes turbopack.root to set it. Output file tracing uses a root to decide what to copy into output: 'standalone', controlled by outputFileTracingRoot. The lockfile warning is the visible symptom of the inference that feeds both, which is why silencing the warning and fixing the missing-files bug are the same action: tell Next.js the root instead of letting it guess.
The fix, in steps
Step 1: Pin the workspace root explicitly
In your app's next.config.js, set the root to the real monorepo root. If your app lives two levels down at apps/web, that is two directories up:
const path = require('path');
/** @type {import('next').NextConfig} */
module.exports = {
output: 'standalone',
// Fixes dev/build root detection and silences the multiple-lockfiles warning
turbopack: {
root: path.join(__dirname, '../../'),
},
// Fixes what standalone output actually copies
outputFileTracingRoot: path.join(__dirname, '../../'),
};
Point both at the folder that holds your canonical lockfile and your shared packages. Now inference is not involved, the warning stops, and tracing starts from the correct root so the shared packages are copied.
Step 2: Remove the stray lockfiles
Pinning the root stops Next.js guessing, but a stray lockfile is still a landmine for every other tool that walks the tree the same way, from your package manager to Turbopack in a different mode. Find them and delete the ones that should not exist:
# List every lockfile in the repo
find . -name 'package-lock.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' \
| grep -v node_modules
You want exactly one, at the workspace root, for the package manager you actually use. Delete a lockfile that a template left inside apps/web. If you find yarn.lock and package-lock.json side by side, one is from a manager you are not using; remove it.
Step 3: Fix the Docker build context
A stray lockfile inside the image is worse than one in the repo, because it appears only in the build and you cannot see it locally. Copy the real lockfile to the real place, and do not let a broad COPY drag a template's lockfile into the app directory:
# Copy the workspace manifests from the root, preserving layout
COPY package.json package-lock.json ./
COPY apps/web/package.json ./apps/web/
COPY packages ./packages
# Build from the app, with the root pinned in next.config.js
RUN npm ci && npx next build apps/web
The principle is that the lockfile layout inside the image must match the layout next.config.js expects. If you pinned the root at ../../, the image must actually have the workspace root two levels above the app, with the one lockfile there.
Verification: look inside the standalone output
Do not trust that a green build means the files shipped. Inspect the output directly.
- Build with
next buildand confirm the warning is gone. Its absence means the root is now pinned, not inferred. - List what tracing copied for a shared package:
find .next/standalone -path '*packages/ui*' | head. If this is empty, the shared package was not traced and your container will crash on it. If it lists files, tracing found the right root. - Run the standalone server locally exactly as production will:
node .next/standalone/apps/web/server.js. Hit a route that imports a shared package. It must render, not throwCannot find module. - Do the same inside the built image, not just on your host, because the whole class of bug is that the image layout differs from your machine.
What people get wrong
Treating it as cosmetic and silencing it without understanding it. The warning is annoying, so people reach for whatever makes it disappear. But if you silence it by pinning the root to the wrong place, you have hidden the symptom and kept the bug: tracing still starts from the wrong folder, files are still missing, and now there is no warning to lead you back. Pin it to the correct root, then verify the output.
Assuming local success proves the build is correct. Locally you have the full node_modules and the full source tree, so nothing is ever missing and the app always runs. Standalone tracing is the only place the wrong root manifests, and standalone is production. A build that works on your laptop tells you nothing about whether the traced output is complete.
Adding files back with a blanket copy instead of fixing the root. When the container crashes on a missing module, the tempting patch is to COPY the whole node_modules into the image and defeat the point of standalone, or to add every missing path to outputFileTracingIncludes one by one. That treats each missing file as its own incident. Fix the root once and tracing finds them all.
Leaving a second lockfile because "it does not hurt anything". It hurts the next tool that walks the tree, and it will reintroduce the wrong-root guess the day someone removes your pin during a refactor. One lockfile, at the root, for one package manager.
When it is still broken
Files still missing after pinning the root. A dependency loaded through a dynamic require or a runtime path that @vercel/nft cannot statically see will not be traced even from the right root. Add just those paths with outputFileTracingIncludes, keyed to the route that needs them, rather than widening the root further. A related version of this untraceable-import problem shows up with logging libraries, which I covered in Turbopack cannot find module with Pino and dynamic requires.
The warning is gone but the build fails differently. If you now hit an ESM or module-format error instead, the root pin exposed a separate monorepo resolution issue rather than causing one; that class of failure is the subject of ERR_REQUIRE_ESM on Vercel but not locally. And if the missing module is a workspace package imported through a path alias, the boundary problem in why path aliases break across transpilePackages may be the real cause.
It only misbehaves in CI. Your CI image or cache may carry a lockfile your local checkout does not. Run the find from Step 2 inside the CI job and print the result, so you are diagnosing the tree CI actually built from, not the one on your desk.
The warning is Next.js being honest that it had to guess. The correct response to a guess about something this consequential is to stop making it guess. Pin the root, keep one lockfile, and check the output before it ships.
Frequently asked questions
- What does 'Found multiple lockfiles' actually mean in Next.js?
- It means Next.js walked up the directory tree, found more than one lockfile (for example a package-lock.json in your app folder and another at the monorepo root), and had to guess which one marks the workspace root. It picks the closest to the current directory. That guess sets the root Next.js uses for its workspace and for output file tracing, so a wrong guess is not cosmetic.
- Why does the wrong lockfile break my standalone Docker build?
- output: 'standalone' copies only the files Next.js traced as needed, relative to the inferred workspace root. If Next.js picks a lockfile in the app folder instead of the real monorepo root, tracing starts too deep and files outside that folder, shared packages, hoisted node_modules, are never copied. The build succeeds and the container starts, then crashes at runtime with a module not found.
- How do I stop Next.js guessing the workspace root?
- Set the root explicitly in next.config.js. Set turbopack.root to your real monorepo root to fix dev and build root detection and silence the warning, and set outputFileTracingRoot to the same path so standalone output tracing starts from the right place. Both should point at the folder that holds the canonical lockfile.
- Should I just ignore the warning if the app runs locally?
- No. Locally your full node_modules is present so nothing looks missing, which is exactly why the footgun is silent. The failure only appears in a standalone or Docker build where tracing decides what to copy. Treat the warning as a signal to pin the root before it costs you a broken deploy.