docs 사이트(docs.1pass.dev, VitePress) 배포가 갑자기 실패했다. Render 대시보드에는 “Exited with status 1 while building your code.” 한 줄만 떴다.
수동으로 “Clear build cache & deploy” 한 번 누르니 그대로 살아났다. 그런데 코드 자체는 한 줄도 안 바뀌었으니 찜찜했다. 무엇이 진짜 문제였는지 확인하고 가야 다음에 같은 상황에서 헤매지 않는다.
증상
Render static site (
pnpm install --frozen-lockfile && pnpm run build)새 커밋 push → 자동 빌드 시작 → 30초 만에
build_failedRender 로그 API에 잡힌 마지막 줄:
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.21.5, puppeteer@24.42.0, vue-demi@0.14.10 Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.그 뒤로 stdout/stderr가 한 줄도 없음. 빌드 프로세스가 무언의 종료.
1차 가설: pnpm의 빌드 스크립트 차단?
ERR_PNPM_IGNORED_BUILDS는 pnpm 9+가 보안상의 이유로 의존성의 postinstall 스크립트를 기본 차단한 결과다. 처음엔 이게 원인인 줄 알았다.
하지만 잘 읽어보면 에러가 아니라 경고다. 차단된 스크립트는:
esbuild— 네이티브 바이너리 다운로드 (postinstall 안 돌아도 npm 패키지에 prebuilt 포함)puppeteer— Chromium 다운로드 (이게 핵심)vue-demi— Vue 2/3 듀얼 빌드 alias 세팅
VitePress가 esbuild의 prebuilt 바이너리를 못 찾으면 빌드가 죽긴 한다. 그런데 같은 lockfile로 다른 환경에서는 멀쩡히 돌았다. 이게 진짜 원인이라기엔 약하다.
2차 가설: 메모리 부족
진짜 의심스러운 건 puppeteer@24.42.0이다. puppeteer가 npm install 단계에서 Chromium(~170MB)을 받아 node_modules/puppeteer/.local-chromium/에 풀어놓는다. Render free static build worker는 메모리/디스크가 빠듯하다.
puppeteer가 정말 빌드에 필요한지 확인해보자:
$ grep -rn "puppeteer" docs-site/ --include="*.mjs" --include="*.js" -l
docs-site/scripts/render-og.mjs
docs-site/scripts/render-og-variants.mjs
scripts/render-og.mjs:
import puppeteer from "puppeteer";
import { dirname, join } from "node:path";
const browser = await puppeteer.launch({ headless: "new" });
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630, deviceScaleFactor: 1 });
await page.goto("file://" + htmlPath, { waitUntil: "networkidle0" });
await page.screenshot({ path: outPath, type: "png" });
await browser.close();
OG 이미지(소셜 미디어 미리보기) 한 번 만들고 끝나는 일회성 스크립트. 결과물(public/og-image.png)은 이미 정적 파일로 커밋되어 있다.
package.json의 build 스크립트를 보면:
"build": "npm run build:llms && vitepress build"
render-og는 호출되지도 않는다. 즉 puppeteer는 빌드 파이프라인에 1도 안 쓰임.
진단
pnpm install --frozen-lockfile 단계에서 puppeteer가 Chromium을 받느라 디스크/메모리/네트워크 부하 → 그 와중에 다른 무언가가 죽음 (esbuild prebuilt 압축 해제 실패 같은) → 빌드 프로세스가 출력 없이 종료.
왜 캐시 청소 후엔 성공했나? stale node_modules 캐시에 깨진 puppeteer 설치본이 남아있다가 frozen-lockfile 모드에서 재구성 실패했을 가능성이 높다. clearCache로 처음부터 다시 받게 하니 정상화됐다.
해결책
puppeteer를 devDependencies에서 빼고, OG 이미지 생성은 별도 워크플로로 분리한다.
Before (docs-site/package.json):
"devDependencies": {
"@scalar/api-reference": "^1.25.0",
"puppeteer": "^24.42.0",
"vitepress": "^1.5.0",
"vue": "^3.5.0"
}
After:
"devDependencies": {
"@scalar/api-reference": "^1.25.0",
"vitepress": "^1.5.0",
"vue": "^3.5.0"
}
OG 이미지를 갱신할 일이 생기면 그때만 ad-hoc으로:
npx puppeteer@24 node scripts/render-og.mjs
또는 GitHub Actions workflow를 따로 만들어서 OG 이미지 갱신 PR만 자동화한다 (workflow_dispatch 트리거).
교훈
- 빌드 시점에 필요 없는 헤비 의존성은 devDependencies에서도 빼라. 일회성 스크립트는
npx나 별도 워크플로로. pnpm install이 받는 패키지 중 postinstall이 큰 것(puppeteer, playwright, sharp, canvas 등)은 항상 의심 대상. Render/Vercel 같은 ephemeral build worker에서는 더더욱.- Render 로그가 출력 없이 끊기면 메모리/디스크 압박을 먼저 의심하라. stderr가 안 잡힐 수 있다.
- build cache clear는 임시 처방. 진짜 원인을 안 잡으면 다음 release 때 재발한다.
메타
- 진단:
curl https://api.render.com/v1/services/{id}/deploys+/v1/logs?ownerId=...&resource=...로 실패한 deploy의 build 로그 끝까지 받아봄 - Render REST API 토큰 한 개로 모든 서비스 로그 접근 가능 — 빌드 실패 디버깅은 대시보드보다 API가 훨씬 빠르다
💬 댓글
비밀번호를 기억해두면 나중에 내 댓글을 삭제할 수 있어요.