![]()
Next.js standalone으로 Docker 이미지 줄이기
Next.js 애플리케이션을 Docker로 배포할 때 이미지가 예상보다 커지는 경우가 많습니다. 소스 코드, 개발 의존성, 전체 node_modules, 빌드 캐시가 최종 이미지에 함께 섞이면 배포 크기와 보안 부담이 같이 늘어납니다.
Next.js의 output: "standalone"은 이 문제를 줄이기 위한 빌드 옵션입니다. 프로덕션 실행에 필요한 파일만 추적해 .next/standalone 폴더에 모아주고, 그 안에는 최소한의 node_modules까지 함께 들어갑니다.
standalone이 하는 일
Next.js 공식 문서는 output: "standalone"을 설정하면 node_modules 설치 없이 단독으로 배포할 수 있는 .next/standalone 폴더와, next start를 대신할 수 있는 최소 server.js가 생성된다고 설명합니다.
const nextConfig = {
output: "standalone",
};
export default nextConfig;
빌드 후에는 .next/standalone 안에 프로덕션 서버를 실행하기 위한 파일이 들어갑니다. 그래서 배포 환경에서는 전체 프로젝트를 복사하지 않고 이 폴더를 중심으로 이미지를 구성할 수 있습니다.
node .next/standalone/server.js
중요한 점은 next start를 쓰는 방식과 다르다는 것입니다. standalone은 빌드 과정에서 추적된 최소 서버를 직접 실행하는 방식에 가깝습니다. 실제로 standalone 빌드 후에는 next CLI에 필요한 의존성이 남지 않기 때문에 next start를 그대로 실행하면 동작하지 않습니다.
public과 static은 따로 챙겨야 합니다
standalone에서 자주 놓치는 부분은 정적 자산입니다. Next.js 문서는 public과 .next/static이 standalone 폴더에 자동으로 복사되지 않으며, 필요하다면 따로 복사해야 한다고 설명합니다.
이 동작은 빠뜨린 것이 아니라 의도된 설계입니다. Next.js는 정적 자산을 standalone 서버가 직접 서빙하기보다 CDN이 처리하는 편이 이상적이라고 보기 때문에, 최소 서버는 이 폴더들을 기본적으로 포함하지 않습니다. 따라서 앞단에 CDN을 두고 정적 자산을 그쪽에서 서빙하는 구성이라면 굳이 복사하지 않아도 됩니다. 반대로 CDN 없이 standalone 서버가 정적 자산까지 직접 책임지는 구성이라면, 아래처럼 두 폴더를 직접 복사해야 server.js가 이를 서빙합니다.
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/
Dockerfile에서도 같은 구조가 필요합니다.
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
이 단계를 빼먹으면 애플리케이션은 뜨는데 이미지, CSS, JS 청크가 404로 깨질 수 있습니다. standalone이 "모든 것을 알아서 넣어준다"는 오해 때문에 자주 생기는 문제입니다.
멀티 스테이지 빌드와 함께 써야 효과가 큽니다
standalone 자체만으로도 필요한 파일을 줄일 수 있지만, Docker에서는 멀티 스테이지 빌드와 함께 사용할 때 효과가 큽니다.
빌드 단계에서는 전체 의존성과 소스 코드가 필요합니다. 하지만 실행 단계에는 빌드 결과와 런타임에 필요한 파일만 있으면 됩니다.
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable pnpm && pnpm install --frozen-lockfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable pnpm && pnpm build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
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"]
실행 이미지에는 빌드 도구와 소스 전체가 남지 않습니다. 이 구조가 이미지 크기, 배포 속도, 취약점 표면을 함께 줄입니다.
실행 단계에서 node server.js로 시작하는 것은 standalone 폴더를 작업 디렉터리 루트(./)로 복사했기 때문입니다. 로컬에서는 node .next/standalone/server.js로 실행하지만, Docker에서는 standalone 폴더 자체가 루트가 되므로 진입점이 server.js가 됩니다.
Docker에서 흔히 겪는 함정: HOSTNAME
standalone 서버를 컨테이너에 띄웠는데 외부에서 접속이 안 되는 경우가 많습니다. 대부분 호스트명 바인딩이 원인입니다.
server.js는 포트와 호스트명을 환경 변수로 받습니다. HOSTNAME을 지정하지 않으면 컨테이너 내부 주소에만 바인딩되어, 호스트나 다른 컨테이너에서 접근하지 못할 수 있습니다. 그래서 Docker 환경에서는 HOSTNAME=0.0.0.0을 명시해 모든 인터페이스에서 수신하도록 해주는 것이 안전합니다.
PORT=3000 HOSTNAME=0.0.0.0 node server.js
위 Dockerfile처럼 ENV로 미리 지정해두면 실행 시 따로 신경 쓰지 않아도 됩니다. 애플리케이션은 정상적으로 뜨는데 접속만 되지 않는다면, 먼저 이 부분을 확인해볼 만합니다.
standalone이 맞지 않는 경우
standalone은 서버 실행이 필요한 Next.js 애플리케이션에 적합합니다. 반대로 정적 export만 필요한 사이트라면 output: "export"나 정적 호스팅 구성이 더 맞을 수 있습니다.
또한 monorepo에서는 파일 추적 범위를 확인해야 합니다. Next.js의 output file tracing은 필요한 파일을 추적하지만, 워크스페이스에서 가져오는 패키지나 외부 패키지 참조 방식에 따라 추가 설정이 필요할 수 있습니다. 런타임에 동적으로 import하는 코드처럼 정적으로 추적되지 않는 부분은 번들에서 누락될 수 있다는 점도 함께 고려해야 합니다.
즉, standalone은 "Docker 배포를 쉽게 해주는 옵션"이지 모든 배포 환경의 정답은 아닙니다.
확인해야 할 체크리스트
Next.js standalone 배포에서 최소한 아래 항목을 확인해야 합니다.
next.config에output: "standalone"이 설정되어 있는가?- 실행 단계에서
.next/standalone을 복사했는가? - (CDN을 쓰지 않는다면)
public과.next/static을 따로 복사했는가? - 컨테이너 명령이
node server.js를 실행하는가? - 런타임 포트와
HOSTNAME=0.0.0.0이 배포 환경에 맞게 지정되어 있는가? - 이미지 안에 불필요한 개발 의존성과 소스가 남지 않았는가?
정리
Next.js standalone은 프로덕션 실행에 필요한 파일만 모아 Docker 이미지를 작게 만드는 데 유용합니다. 하지만 정적 자산까지 자동으로 완성해주는 마법은 아닙니다.
Docker에서 standalone을 쓸 때 핵심은 네 가지입니다. 빌드와 실행 단계를 분리하고, standalone 폴더를 실행 이미지로 가져오고, 필요하다면 public과 .next/static을 따로 복사하고, HOSTNAME을 포함한 런타임 환경 변수를 배포 환경에 맞게 지정하는 것입니다.
배포 이미지를 줄이고 싶다면 단순히 베이스 이미지를 alpine으로 바꾸는 것보다, 먼저 최종 이미지에 무엇이 들어가야 하는지부터 좁혀야 합니다.
참고
- Next.js Docs — output
- Docker Docs — Containerize a Next.js application
참고
Read more
DMARC, 이메일 도메인을 피싱에서 지키는 최소한의 정책
SPF와 DKIM만으로 부족한 이유, DMARC가 인증 실패 메일을 어떻게 처리하고 보고서를 통해 운영 상태를 보여주는지 정리합니다.
EKS IRSA, Pod에 AWS 권한을 나눠주는 현실적인 방법
EKS에서 IRSA가 어떤 보안 문제를 해결하는지, OIDC와 ServiceAccount를 통해 Pod 단위 IAM 권한이 연결되는 방식을 정리합니다.
LSP는 에디터와 언어의 M×N 문제를 어떻게 줄였나
Language Server Protocol이 왜 등장했는지, 에디터와 언어 서버가 어떤 방식으로 통신하는지, 개발 도구와 AI 도구에서 왜 중요한지 정리합니다.