
pnpm: 더 빠르고 디스크 효율적인 패키지 매니저
node_modules가 1GB를 넘어가고 npm install이 5분 걸리던 프로젝트가, pnpm으로 바꾸니 용량도 속도도 절반이 됐다.

node_modules가 1GB를 넘어가고 npm install이 5분 걸리던 프로젝트가, pnpm으로 바꾸니 용량도 속도도 절반이 됐다.
서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

내 서버가 해킹당하지 않는 이유. 포트와 IP를 검사하는 '패킷 필터링'부터 AWS Security Group까지, 방화벽의 진화 과정.

왜 넷플릭스는 멀쩡한 서버를 랜덤하게 꺼버릴까요? 시스템의 약점을 찾기 위해 고의로 장애를 주입하는 카오스 엔지니어링의 철학과 실천 방법(GameDay)을 소개합니다.

프로젝트 3개를 동시에 작업하다 보니 맥북 용량이 빠듯해졌다. 디스크 분석기를 돌려봤더니 각 프로젝트의 node_modules가 1.2GB씩 차지하고 있었다. 같은 라이브러리를 쓰는데도 프로젝트마다 따로 설치되어 있었다. React 18.2.0을 세 번, lodash를 네 번, TypeScript를 다섯 번 설치한 셈이다.
더 짜증 나는 건 속도였다. npm install을 실행하면 커피 한 잔 마시고 와야 했다. CI/CD 파이프라인에서도 패키지 설치만 5분이 걸렸다. 빌드 시간의 절반 이상이 의존성 설치였다.
그러던 중 동료가 pnpm을 추천했다. "npm이랑 똑같은데 훨씬 빠르고 용량도 적게 먹어"라는 말에 반신반의하며 시도해봤는데, 결과는 충격적이었다. 설치 시간 5분 → 1분 30초, node_modules 크기 1.2GB → 400MB. 마법 같았다.
pnpm의 핵심은 단순하다. 같은 패키지를 여러 번 저장하지 않는다는 것. 대신 한 곳에 저장해두고 필요할 때마다 링크로 연결한다.
npm/yarn을 쓰는 건 이런 상황과 같다:
pnpm은 이렇게 작동한다:
이게 바로 content-addressable store의 개념이다.
pnpm은 모든 패키지를 ~/.pnpm-store라는 글로벌 저장소에 보관한다. 각 패키지는 해시값으로 식별된다:
~/.pnpm-store/v3/files/
├── 00/
│ ├── 1a2b3c... (react@18.2.0의 실제 파일들)
│ └── 4d5e6f... (lodash@4.17.21의 실제 파일들)
└── 01/
└── 7g8h9i...
프로젝트의 node_modules는 이렇게 생겼다:
node_modules/
├── .pnpm/
│ ├── react@18.2.0/
│ │ └── node_modules/
│ │ └── react -> ~/.pnpm-store/.../1a2b3c...
│ └── lodash@4.17.21/
│ └── node_modules/
│ └── lodash -> ~/.pnpm-store/.../4d5e6f...
├── react -> .pnpm/react@18.2.0/node_modules/react
└── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
실제 파일은 글로벌 스토어에 하나만 있고, 프로젝트에는 링크만 있다.
pnpm을 이해하려면 두 가지 링크 개념을 알아야 한다. 처음엔 어렵게 느껴졌지만, 파일 시스템의 기본 원리만 알면 쉽다.
파일 시스템에서 파일은 두 부분으로 나뉜다:
Hard link는 같은 실제 데이터를 가리키는 또 다른 이름이다:
# 원본 파일
echo "Hello" > original.txt
# Hard link 생성
ln original.txt hardlink.txt
# 둘은 완전히 같은 데이터를 가리킴
ls -li
# 12345678 -rw-r--r-- 2 user staff 6 original.txt
# 12345678 -rw-r--r-- 2 user staff 6 hardlink.txt
# ↑ inode 번호가 같다!
original.txt를 지워도 hardlink.txt는 여전히 데이터에 접근할 수 있다. 왜냐하면 실제 데이터는 모든 hard link가 사라질 때까지 지워지지 않기 때문이다.
pnpm은 글로벌 스토어에서 프로젝트로 패키지를 복사할 때 hard link를 사용한다. 실제 파일 데이터는 한 번만 디스크에 존재하지만, 여러 프로젝트에서 접근할 수 있다.
Symlink(심볼릭 링크)는 "바로가기"다. 실제 데이터가 아니라 다른 파일의 경로만 저장한다:
# Symlink 생성
ln -s /path/to/original.txt symlink.txt
# symlink.txt를 읽으면 자동으로 original.txt로 리다이렉트됨
ls -l
# lrwxr-xr-x 1 user staff 20 symlink.txt -> /path/to/original.txt
pnpm은 프로젝트의 node_modules에서 .pnpm 디렉토리로 symlink를 건다:
// 코드에서는 이렇게 import하지만
import React from 'react';
// 실제로는 이런 경로를 따라간다:
// node_modules/react (symlink)
// → node_modules/.pnpm/react@18.2.0/node_modules/react (hard link)
// → ~/.pnpm-store/v3/files/.../react (실제 파일)
Hard link만 쓰면 의존성 트리를 표현하기 어렵고, symlink만 쓰면 디스크 공간을 절약할 수 없다. 둘을 조합해야 pnpm의 마법이 완성된다.
npm/yarn을 쓰다가 이런 경험 없었나? 분명 package.json에 없는 패키지인데 import가 되는 상황. 이게 바로 phantom dependency 문제다.
npm v3부터는 의존성을 평평하게(flat) 설치한다:
# package.json에 express만 있어도
{
"dependencies": {
"express": "^4.18.0"
}
}
# node_modules는 이렇게 됨
node_modules/
├── express/
├── body-parser/ # express의 의존성
├── cookie-parser/ # express의 의존성
└── debug/ # 여러 패키지가 공통으로 쓰는 의존성
이 구조에서는 body-parser를 직접 import할 수 있다:
// package.json에 없는데도 작동함!
import bodyParser from 'body-parser';
문제는 나중에 express가 body-parser를 제거하거나 버전을 바꾸면 내 코드가 깨진다는 것. 나는 body-parser에 의존하는지도 몰랐는데 말이다.
pnpm은 각 패키지를 완전히 격리한다:
node_modules/
├── .pnpm/
│ ├── express@4.18.0/
│ │ └── node_modules/
│ │ ├── express/
│ │ ├── body-parser/
│ │ └── cookie-parser/
│ └── body-parser@1.20.0/
│ └── node_modules/
│ └── body-parser/
└── express -> .pnpm/express@4.18.0/node_modules/express
node_modules 최상위에는 package.json에 명시된 패키지만 존재한다. body-parser는 .pnpm/express@4.18.0/node_modules/ 안에 갇혀 있어서 직접 접근할 수 없다.
// 에러 발생!
import bodyParser from 'body-parser';
// Error: Cannot find module 'body-parser'
처음엔 불편했다. 기존 프로젝트를 pnpm으로 마이그레이션할 때 숨어있던 phantom dependency들이 줄줄이 터졌다. 하지만 이게 오히려 좋았다. 실제로 사용하는 의존성을 명확히 알게 됐고, package.json이 진짜 의존성 목록이 됐다.
실제로 프로젝트를 마이그레이션하면서 겪은 과정을 정리했다.
가장 간단한 방법은 Corepack을 사용하는 것:
# Node.js 16.13+ 부터는 Corepack이 기본 포함됨
corepack enable
# pnpm 버전 지정 (선택사항)
corepack prepare pnpm@latest --activate
# 설치 확인
pnpm --version
또는 직접 설치:
npm install -g pnpm
# 또는
curl -fsSL https://get.pnpm.io/install.sh | sh -
# node_modules와 lock 파일 제거
rm -rf node_modules
rm package-lock.json # npm
rm yarn.lock # yarn
pnpm install
이때 phantom dependency 에러가 발생할 수 있다:
Error: Cannot find module 'some-package'
해결 방법은 간단하다. 실제로 사용하는 패키지를 명시적으로 추가:
pnpm add some-package
일부 레거시 패키지나 peer dependency 문제가 있을 때:
# .npmrc 파일 생성
echo "shamefully-hoist=true" > .npmrc
shamefully-hoist=true는 npm과 비슷하게 패키지를 평평하게 만든다. 완벽한 해결책은 아니지만 급한 불은 끈다.
내 Next.js 프로젝트 기준:
| 항목 | npm | pnpm | 개선 |
|---|---|---|---|
| 설치 시간 (캐시 없음) | 4분 32초 | 1분 18초 | 71% ↓ |
| 설치 시간 (캐시 있음) | 38초 | 12초 | 68% ↓ |
| node_modules 크기 | 1.2GB | 380MB | 68% ↓ |
| 설치된 파일 수 | 89,432개 | 31,847개 | 64% ↓ |
여러 패키지를 하나의 레포지토리에서 관리하는 monorepo에서 pnpm은 특히 빛난다.
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'
my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── apps/
│ ├── web/ # Next.js 앱
│ │ └── package.json
│ └── api/ # Express API
│ └── package.json
└── packages/
├── ui/ # 공유 UI 컴포넌트
│ └── package.json
└── utils/ # 공유 유틸리티
└── package.json
apps/web/package.json:
{
"name": "web",
"dependencies": {
"@my-org/ui": "workspace:*",
"@my-org/utils": "workspace:*",
"next": "^14.0.0"
}
}
workspace:* 프로토콜을 사용하면 로컬 패키지를 참조한다. 심볼릭 링크로 연결되어 실시간으로 변경사항이 반영된다.
# web 앱만 빌드
pnpm --filter web build
# ui 패키지와 그걸 사용하는 모든 패키지 테스트
pnpm --filter @my-org/ui... test
# api를 제외한 모든 패키지 설치
pnpm install --filter=!api
monorepo에서 여러 앱이 같은 라이브러리를 쓸 때, pnpm은 딱 한 번만 설치한다:
# apps/web과 apps/api 둘 다 react@18.2.0을 쓰면
# ~/.pnpm-store에 react는 1개만 존재하고
# 각 앱은 그걸 링크로만 참조함
# 이전에 Lerna + yarn을 쓸 때:
# apps/web/node_modules/react (500MB)
# apps/api/node_modules/react (500MB)
# 총 1GB
# pnpm으로 바꾼 후:
# ~/.pnpm-store/react (500MB)
# apps/web/node_modules/react -> 링크
# apps/api/node_modules/react -> 링크
# 총 500MB
실제 프로젝트에서 측정한 결과들.
# 모든 캐시 삭제 후
rm -rf node_modules ~/.npm ~/.pnpm-store ~/.yarn
# npm
time npm install
# 4분 32초
# yarn (classic)
time yarn install
# 2분 18초
# pnpm
time pnpm install
# 1분 18초
# node_modules만 삭제
# npm
time npm install
# 38초
# yarn
time yarn install
# 24초
# pnpm
time pnpm install
# 12초
GitHub Actions에서 측정:
# .github/workflows/ci.yml
- name: Install dependencies
run: pnpm install --frozen-lockfile
| 패키지 매니저 | 평균 설치 시간 | 캐시 적중률 |
|---|---|---|
| npm | 2분 47초 | 65% |
| yarn | 1분 52초 | 72% |
| pnpm | 54초 | 88% |
pnpm의 압도적인 승리. 캐시 효율이 높아서 같은 패키지를 반복 설치할 때 특히 빠르다.
프로젝트에 pnpm을 도입할 때 가장 큰 장벽은 "팀원들도 pnpm을 설치해야 한다"는 것이었다. Corepack이 이 문제를 해결했다.
Node.js 16.13+에 기본 포함된 패키지 매니저 관리 도구. package.json에 명시된 패키지 매니저를 자동으로 설치하고 사용한다.
// package.json
{
"packageManager": "pnpm@8.15.0"
}
# Corepack 활성화 (한 번만)
corepack enable
# 이제 pnpm 명령이 자동으로 작동
pnpm install
팀원이 npm이나 yarn을 실행하면 경고가 뜬다:
$ npm install
Usage Error: This project is configured to use pnpm
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
Corepack 덕분에 별도로 pnpm을 설치할 필요가 없다.
pnpm으로 전환하면서 마주친 문제들과 해결 방법.
문제: 기존 코드가 phantom dependency에 의존하고 있을 때
// package.json에 없는데 import했음
import _ from 'lodash';
// Error: Cannot find module 'lodash'
해결:
# 명시적으로 추가
pnpm add lodash
문제: node-gyp로 빌드하는 패키지가 symlink를 따라가지 못함
# sharp, canvas 같은 패키지 설치 시 에러
해결: .npmrc에 추가
node-linker=hoisted
또는 특정 패키지만:
public-hoist-pattern[]=*sharp*
문제: Next.js 13+에서 가끔 SWC가 심볼릭 링크를 제대로 처리 못함
해결: next.config.js에 추가
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// pnpm symlink 지원
esmExternals: 'loose',
},
}
module.exports = nextConfig
문제: 심볼릭 링크가 Docker 레이어 캐싱과 충돌
해결: Multi-stage build 패턴
# Dockerfile
FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
문제: pnpm이 peer dependency를 엄격하게 체크해서 경고가 많음
WARN @typescript-eslint/parser requires a peer of eslint@^8.0.0 but none is installed.
해결: 실제로 필요한 것만 설치하거나, .npmrc로 경고 무시
# 엄격한 peer dependency 체크 비활성화
strict-peer-dependencies=false
6개월간 pnpm을 사용하면서 느낀 점을 정리하면:
새 프로젝트라면 무조건 pnpm을 추천한다. 기존 프로젝트도 마이그레이션 비용이 크지 않다면 전환할 가치가 충분하다.
특히 이런 상황이라면 당장 시도해봐야 한다:
npm install이 끝나기를 기다리며 커피를 마시던 시간을, 이제는 실제 개발에 쓸 수 있게 됐다. 그것만으로도 충분한 가치다.
Working on three projects simultaneously, my MacBook's storage got tight. Running a disk analyzer revealed each project's node_modules consuming 1.2GB. The same libraries were installed separately for each project. React 18.2.0 three times, lodash four times, TypeScript five times.
What frustrated me more was the speed. Running npm install meant grabbing a coffee and waiting. Even in CI/CD pipelines, package installation alone took 5 minutes. More than half the build time went to dependency installation.
Then a colleague recommended pnpm. "It's just like npm but way faster and uses less space," they said. Skeptical but curious, I tried it. The results were shocking. Installation time: 5 minutes → 1 minute 30 seconds. node_modules size: 1.2GB → 400MB. Pure magic.
pnpm's core principle is simple: never store the same package twice. Instead, store it once and link to it when needed.
Using npm/yarn is like this scenario:
pnpm works like this:
This is the concept of a content-addressable store.
pnpm stores all packages in a global store at ~/.pnpm-store. Each package is identified by its hash:
~/.pnpm-store/v3/files/
├── 00/
│ ├── 1a2b3c... (actual files of react@18.2.0)
│ └── 4d5e6f... (actual files of lodash@4.17.21)
└── 01/
└── 7g8h9i...
The project's node_modules looks like this:
node_modules/
├── .pnpm/
│ ├── react@18.2.0/
│ │ └── node_modules/
│ │ └── react -> ~/.pnpm-store/.../1a2b3c...
│ └── lodash@4.17.21/
│ └── node_modules/
│ └── lodash -> ~/.pnpm-store/.../4d5e6f...
├── react -> .pnpm/react@18.2.0/node_modules/react
└── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash
The actual files exist only once in the global store, while projects contain only links.
Understanding pnpm requires grasping two linking concepts. Initially daunting, but straightforward once you understand basic filesystem principles.
In a filesystem, a file consists of two parts:
A hard link is another name pointing to the same actual data:
# Original file
echo "Hello" > original.txt
# Create hard link
ln original.txt hardlink.txt
# Both point to identical data
ls -li
# 12345678 -rw-r--r-- 2 user staff 6 original.txt
# 12345678 -rw-r--r-- 2 user staff 6 hardlink.txt
# ↑ Same inode number!
Even if you delete original.txt, hardlink.txt still accesses the data. The actual data isn't deleted until all hard links disappear.
pnpm uses hard links when copying packages from the global store to projects. The actual file data exists only once on disk, but multiple projects can access it.
A symlink (symbolic link) is a "shortcut". It stores only the path to another file, not the actual data:
# Create symlink
ln -s /path/to/original.txt symlink.txt
# Reading symlink.txt automatically redirects to original.txt
ls -l
# lrwxr-xr-x 1 user staff 20 symlink.txt -> /path/to/original.txt
pnpm creates symlinks from a project's node_modules to the .pnpm directory:
// Code imports like this
import React from 'react';
// But actually follows this path:
// node_modules/react (symlink)
// → node_modules/.pnpm/react@18.2.0/node_modules/react (hard link)
// → ~/.pnpm-store/v3/files/.../react (actual files)
Using only hard links makes dependency trees difficult to represent. Using only symlinks doesn't save disk space. Combining both completes pnpm's magic.
Ever experienced this with npm/yarn? Importing a package that's definitely not in package.json, yet it works. That's the phantom dependency problem.
Since npm v3, dependencies are installed flat:
# Even with only express in package.json
{
"dependencies": {
"express": "^4.18.0"
}
}
# node_modules becomes this
node_modules/
├── express/
├── body-parser/ # express's dependency
├── cookie-parser/ # express's dependency
└── debug/ # common dependency shared by multiple packages
In this structure, you can directly import body-parser:
// Works even though not in package.json!
import bodyParser from 'body-parser';
The problem: if express later removes body-parser or changes versions, your code breaks. You didn't even know you depended on body-parser.
pnpm completely isolates each package:
node_modules/
├── .pnpm/
│ ├── express@4.18.0/
│ │ └── node_modules/
│ │ ├── express/
│ │ ├── body-parser/
│ │ └── cookie-parser/
│ └── body-parser@1.20.0/
│ └── node_modules/
│ └── body-parser/
└── express -> .pnpm/express@4.18.0/node_modules/express
Only packages explicitly declared in package.json exist at the node_modules top level. body-parser is trapped inside .pnpm/express@4.18.0/node_modules/ and cannot be accessed directly.
// Error thrown!
import bodyParser from 'body-parser';
// Error: Cannot find module 'body-parser'
Initially inconvenient. Migrating existing projects to pnpm exposed hidden phantom dependencies one after another. But this turned out beneficial. I gained clarity on actual dependencies, and package.json became the true dependency list.
Here's the process I went through migrating actual projects.
The simplest method uses Corepack:
# Node.js 16.13+ includes Corepack by default
corepack enable
# Specify pnpm version (optional)
corepack prepare pnpm@latest --activate
# Verify installation
pnpm --version
Or install directly:
npm install -g pnpm
# or
curl -fsSL https://get.pnpm.io/install.sh | sh -
# Remove node_modules and lock files
rm -rf node_modules
rm package-lock.json # npm
rm yarn.lock # yarn
pnpm install
Phantom dependency errors might occur:
Error: Cannot find module 'some-package'
The solution is simple. Explicitly add packages you actually use:
pnpm add some-package
For legacy packages or peer dependency issues:
# Create .npmrc file
echo "shamefully-hoist=true" > .npmrc
shamefully-hoist=true flattens packages similarly to npm. Not a perfect solution, but puts out immediate fires.
Based on my Next.js project:
| Metric | npm | pnpm | Improvement |
|---|---|---|---|
| Install time (no cache) | 4m 32s | 1m 18s | 71% ↓ |
| Install time (with cache) | 38s | 12s | 68% ↓ |
| node_modules size | 1.2GB | 380MB | 68% ↓ |
| Installed files | 89,432 | 31,847 | 64% ↓ |
In monorepos managing multiple packages in one repository, pnpm particularly excels.
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'
my-monorepo/
├── pnpm-workspace.yaml
├── package.json
├── apps/
│ ├── web/ # Next.js app
│ │ └── package.json
│ └── api/ # Express API
│ └── package.json
└── packages/
├── ui/ # Shared UI components
│ └── package.json
└── utils/ # Shared utilities
└── package.json
apps/web/package.json:
{
"name": "web",
"dependencies": {
"@my-org/ui": "workspace:*",
"@my-org/utils": "workspace:*",
"next": "^14.0.0"
}
}
Using the workspace:* protocol references local packages. They're connected via symbolic links, so changes reflect in real-time.
# Build only the web app
pnpm --filter web build
# Test the ui package and all packages that use it
pnpm --filter @my-org/ui... test
# Install all packages except api
pnpm install --filter=!api
In a monorepo when multiple apps use the same library, pnpm installs it exactly once:
# If both apps/web and apps/api use react@18.2.0
# Only 1 copy of react exists in ~/.pnpm-store
# Each app only references it via link
# Previously with Lerna + yarn:
# apps/web/node_modules/react (500MB)
# apps/api/node_modules/react (500MB)
# Total: 1GB
# After switching to pnpm:
# ~/.pnpm-store/react (500MB)
# apps/web/node_modules/react -> link
# apps/api/node_modules/react -> link
# Total: 500MB
Results measured from actual projects.
# After clearing all caches
rm -rf node_modules ~/.npm ~/.pnpm-store ~/.yarn
# npm
time npm install
# 4m 32s
# yarn (classic)
time yarn install
# 2m 18s
# pnpm
time pnpm install
# 1m 18s
# Only delete node_modules
# npm
time npm install
# 38s
# yarn
time yarn install
# 24s
# pnpm
time pnpm install
# 12s
Measured in GitHub Actions:
# .github/workflows/ci.yml
- name: Install dependencies
run: pnpm install --frozen-lockfile
| Package Manager | Average Install Time | Cache Hit Rate |
|---|---|---|
| npm | 2m 47s | 65% |
| yarn | 1m 52s | 72% |
| pnpm | 54s | 88% |
pnpm's overwhelming victory. High cache efficiency makes it especially fast when repeatedly installing the same packages.
The biggest barrier when introducing pnpm to a project was "teammates also need to install pnpm." Corepack solved this problem.
A package manager management tool included by default in Node.js 16.13+. It automatically installs and uses the package manager specified in package.json.
// package.json
{
"packageManager": "pnpm@8.15.0"
}
# Enable Corepack (once only)
corepack enable
# Now pnpm commands work automatically
pnpm install
If teammates run npm or yarn, they get a warning:
$ npm install
Usage Error: This project is configured to use pnpm
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
Thanks to Corepack, no need to separately install pnpm.
Problems encountered while switching to pnpm and their solutions.
Problem: When existing code depends on phantom dependencies
// Imported without being in package.json
import _ from 'lodash';
// Error: Cannot find module 'lodash'
Solution:
# Add explicitly
pnpm add lodash
Problem: Packages building with node-gyp can't follow symlinks
# Errors when installing packages like sharp, canvas
Solution: Add to .npmrc
node-linker=hoisted
Or for specific packages only:
public-hoist-pattern[]=*sharp*
Problem: Next.js 13+ occasionally has SWC not properly handling symbolic links
Solution: Add to next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// pnpm symlink support
esmExternals: 'loose',
},
}
module.exports = nextConfig
Problem: Symbolic links conflict with Docker layer caching
Solution: Multi-stage build pattern
# Dockerfile
FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
Problem: pnpm strictly checks peer dependencies, causing many warnings
WARN @typescript-eslint/parser requires a peer of eslint@^8.0.0 but none is installed.
Solution: Install only what's actually needed, or ignore warnings via .npmrc
# Disable strict peer dependency checking
strict-peer-dependencies=false