Next.js용 Docker
앱 이미지에 사용합니다. Next-side standalone 사실은 nextjs-production(references/self-hosting.md)에 있습니다. Kubernetes에서 이미지를 실행하는 방법은 helm-nextjs에, CI에서 빌드 및 푸시하는 방법은 github-actions에 있습니다.
템플릿의 apps/<app>/Dockerfile이 진실 공급원(source of truth)입니다. references/Dockerfile.md에 전체 내용과 주석이 있습니다. 저장소 루트에서 빌드: docker build -f apps/<app>/Dockerfile -t <app> .
규칙
-
다섯 개의 스테이지:
base→pruner→deps→builder→runner.base: Node + pnpm, 그 외에는 아무것도 없음.pruner: 전체 저장소에서turbo prune <app> --docker를 실행하여 앱과 해당 워크스페이스 종속성만 남깁니다.deps:out/json/(매니페스트)과out/pnpm-lock.yaml만 복사한 다음,pnpm fetch+pnpm install --frozen-lockfile --offline을 실행합니다. 이 레이어는 매니페스트나 lockfile이 변경될 때까지 재사용되므로 소스 코드 편집이 재설치를 유발하지 않습니다.builder:out/full/(소스)을 복사하고pnpm exec turbo run build --filter=<app>를 실행합니다.runner: standalone 출력만 포함하는 새로운 Node 이미지입니다. 이유: 소스 변경은 마지막 두 단계만 다시 빌드하며, runner에는 pnpm, 소스, devDependencies가 없습니다.
-
Node 이미지를 정확한 버전과 다이제스트로 고정하고,
.node-version과 일치시킵니다.node:26.12.0-alpine3.24@sha256:6f08…을 하나의ARG NODE_IMAGE에 사용하며,base와runner모두에서 사용합니다. 다이제스트는 멀티 아키텍처 인덱스이므로 amd64와 arm64가 모두 확인됩니다. Alpine을 distroless 대신 사용하는 이유: distroless Node는 부동 태그(latest,nonroot)만 게시하므로.node-version과 정확한 Node 버전을 일치시킬 수 없고 HEALTHCHECK를 위한wget도 없습니다. Alpine은 작고, 정확하게 고정할 수 있으며,sharp가 사전 빌드된 musl 바이너리를 제공하므로 추가 패키지 없이 이미지 최적화가 작동합니다. -
pnpm을 GitHub 릴리스 바이너리에서 설치하고 SHA-256으로 검증합니다. 절대
npm i -g pnpm, Corepack, 또는 검증되지 않은curl | sh를 사용하지 마십시오.packageManager와 동일한 하나의ARG PNPM_VERSION과 아키텍처별pnpm-linux-{x64,arm64}-musl.tar.gz에 대한 체크섬 하나,TARGETARCH로 선택합니다. 업데이트하려면: 릴리스의 자산 다이제스트(gh release view v<version> --repo pnpm/pnpm --json assets)에서 sha256 값을 가져와 세 개의 ARG를 변경하고, 같은 커밋에서packageManager도 업데이트합니다. 릴리스 형식이 변경될 경우의 대비:pruner/deps/builder스테이지를ghcr.io/pnpm/pnpm:<version>(Node 없음, 추가 필요) 기반으로 하여 새로운 설치 방법을 고안하지 않습니다. -
runner는 정확히 세 가지를 복사합니다.
COPY --from=builder /repo/apps/<app>/.next/standalone ./ COPY --from=builder /repo/apps/<app>/.next/static ./apps/<app>/.next/static COPY --from=builder /repo/apps/<app>/public ./apps/<app>/publicstandalone은
static또는public을 포함하지 않습니다. 이것들이 없으면 앱은 JS나 CSS를 제공하지 않습니다. 모노레포에서 서버는apps/<app>/server.js입니다.CMD ["node", "apps/<app>/server.js"], exec 형식이므로 Node가 직접 SIGTERM을 수신합니다. -
uid/gid 1001로 실행하고 파일은 root가 소유합니다.
nextjs:nodejs(1001:1001) 사용자를 생성하고, 복사된 파일은 root 소유로 둡니다(앱은 읽을 수 있지만 자체 코드를 변경할 수 없음).apps/<app>/.next/cache만chown합니다.USER 1001:1001, 숫자 형식이므로 Kubernetes가 이름 확인 없이runAsNonRoot를 확인할 수 있습니다. -
이미지는 읽기 전용 루트 파일시스템에서 작동해야 합니다. 유일한 런타임 쓰기는
.next/cache(이미지 최적화, ISR/패치 캐시)로 이동합니다. Kubernetes는 여기에emptyDir을 마운트합니다. 로컬에서는--tmpfs를 사용합니다. 다음 명령으로 테스트:docker run --rm -p 3000:3000 --read-only \ --tmpfs /app/apps/<app>/.next/cache:uid=1001,gid=1001 \ --cap-drop ALL --security-opt no-new-privileges <app> -
runner에서
NODE_ENV=production,HOSTNAME=0.0.0.0,PORT=3000,NEXT_TELEMETRY_DISABLED=1을 설정합니다.HOSTNAME=0.0.0.0이 없으면 standalone 서버가 컨테이너 호스트 이름에 바인딩되어 프로브가 실패합니다. -
HEALTHCHECK는 busyboxwget으로/api/health를 확인합니다.docker run/Compose용입니다. Kubernetes는 이를 무시하고 동일한 경로에 차트의 프로브를 사용합니다. -
빌드 인자는 빌드 시 인라인 처리되는 값(
NEXT_PUBLIC_*및NEXT_DEPLOYMENT_ID)으로만 제한됩니다.NEXT_PUBLIC_*는 JS에 컴파일되므로builder에서ARG+ENV여야 하며, 이미지가 실행되는 모든 환경에서 동일합니다. 환경별 모든 것은 차트의 런타임 환경 변수입니다. 추가하려면:ARG NEXT_PUBLIC_APP_NAME ENV NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME}…그리고 두 워크플로우에
build-args를 추가하고,NEXT_PUBLIC_*는 이미 turbo의env에 있습니다. -
시크릿은 절대
ARG,ENV또는COPY에 넣지 않습니다. 이미지 히스토리에 남게 됩니다. BuildKit 시크릿 마운트를 사용하면 해당RUN동안에만 존재합니다:RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN pnpm fetch # private registry템플릿의 빌드 단계는 이미 선택적
next_server_actions_key시크릿을 이 방식으로 선언합니다. -
BuildKit 캐시 마운트로 pnpm 스토어를 캐시합니다.
fetch/install/dlx에서--mount=type=cache,id=pnpm-store,target=/pnpm/store를 사용하고pnpm_config_store_dir=/pnpm/store를 설정합니다. 스토어는 동일한 빌더에서 빌드 간에 유지되며 레이어에 포함되지 않습니다. CI에서는 레이어 캐싱이type=gha입니다(github-actions참조). -
저장소 루트의
.dockerignore는 컨텍스트를 작게 유지하고 시크릿을 제외합니다.node_modules,.next,.turbo,dist,coverage,.env*,.git,.github,charts를 제외합니다. Windows 또는 macOS에서node_modules를 포함하는 컨텍스트는 Linux 이미지에서 네이티브 패키지를 손상시킵니다. -
멀티 아키텍처는
linux/amd64,linux/arm64이며 CI에서 빌드합니다. 로컬에서는 자신의 아키텍처로 빌드하십시오. 모든 스테이지는 pnpm 다운로드를 제외하고 아키텍처 중립적이며, pnpm 다운로드는TARGETARCH에서 체크섬을 선택합니다. -
레이블: Dockerfile은
org.opencontainers.image.title및licenses를 설정합니다. CI는docker/metadata-action을 통해source,revision,version,created를 추가합니다. 버전 레이블을 하드코딩하지 마십시오. -
스테이지를 앱별 핵(hack) 없이 유지합니다. runner에
apk add(앱에 셸 도구 세트가 필요하지 않음),npm, 호스트에서 복사된node_modules가 없어야 합니다. 앱이 런타임에 네이티브 도구를 필요로 하는 경우 주석으로 정당화하십시오.
디버깅
| 증상 | 원인 |
|---|---|
빌드 중 extends: … doesn't resolve | 앱에 필요한 루트 수준 파일이 정리된 트리에 없음: 워크스페이스 패키지로 이동 |
deps에서 ERR_PNPM_OUTDATED_LOCKFILE | 매니페스트 변경 후 lockfile이 커밋되지 않음 |
ERR_PNPM_IGNORED_BUILDS | 새 종속성이 install 스크립트를 필요로 함: allowBuilds에서 결정 |
| 앱 시작, CSS/JS 없음 | .next/static 또는 public이 apps/<app>/에 복사되지 않음 |
| 상태 확인 실패, 로그는 정상 | HOSTNAME이 0.0.0.0이 아님 |
런타임 EROFS | .next/cache 외부에 쓰는 항목이 있음: 찾아서 수정, --read-only를 포기하지 마십시오 |
NEXT_PUBLIC_X가 비어 있음 | 빌드 인자로 전달되지 않음(빌드 시 주입됨) |
검증
docker build -f apps/<app>/Dockerfile -t <app>:local --build-arg NEXT_DEPLOYMENT_ID=local .
docker run -d --name <app> -p 3000:3000 --read-only --tmpfs /app/apps/<app>/.next/cache:uid=1001,gid=1001 --cap-drop ALL <app>:local
curl -fsS localhost:3000/api/health
docker exec <app> id # uid=1001(nextjs)
docker inspect --format '{{.State.Health.Status}}' <app> # ~30초 후 healthy
docker rm -f <app>
출처
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://turborepo.dev/docs/guides/tools/docker
- https://pnpm.io/docker
- https://pnpm.io/cli/fetch
- https://github.com/pnpm/pnpm/releases
- https://docs.docker.com/build/building/best-practices/
- https://docs.docker.com/build/building/secrets/
- https://docs.docker.com/build/cache/optimize/#use-cache-mounts
- https://docs.docker.com/reference/dockerfile/#healthcheck
- https://hub.docker.com/_/node
- https://github.com/opencontainers/image-spec/blob/main/annotations.md