본문 바로가기

개발 일지

[Next.js] Next 앱 도커 이미지 다이어트하기

node 기반의 앱이 항상 겪어야 하는 문제가 있다. 바로 node_modules 라는 짐을 지고 가야 한다는 것이다. Next.js 로 작성된 웹 페이지는 server side rendering 을 포함하기 때문에 React 처럼 static page 로 export 해서 가볍게 배포할 수가 없다. node_modules 의 기막힌 무거움을 해결하기 위해 pnpm 이나 yarn berry 같은 해결책이 나왔지만, docker image 로 빌드해서 배포하는 경우, 컨테이너 안에 node dependency 를 필연적으로 들고 있어야 해서 image 의 크기가 커지고 만다.

[출처] https://www.reddit.com/r/ProgrammerHumor/comments/6s0wov/heaviest_objects_in_the_universe/

 

Docker Image 의 크기가 커지는 것이 무슨 상관인가 싶지만, 배포 과정에서 push / pull 하는 데에도 시간이 오래 걸리고, 크기가 큰 이미지를 저장하고 있어야 하니, 용량도 많이 차지하게 되어, 생각보다 불편한 점이 많다. Next.js 앱을 docker 를 이용해서 배포할 때에, 이미지 사이즈를 최적화하는 방법을 알게 되어 공유한다.

Create Next App

먼저 create-next-app 을 이용해서 가장 기본적인 next application 을 생성해 보자.

$ yarn create next-app my-next-app

Would you like to use TypeScript?  Yes  
Would you like to use ESLint?  Yes  
Would you like to use Tailwind CSS?  No  
Would you like to use \`src/\` directory?  Yes  
Would you like to use App Router? (recommended)  Yes  
Would you like to customize the default import alias (@/\*)?  No

 

기본 설정대로 따라가다 보면, my-next-app 이라는 폴더에 next 앱이 생성되었을 것이다.

무거운 이미지 만들기

대조군 설정을 위해서 고의적으로 전혀 최적화되지 않은 이미지를 만들어 보자. 먼저, 간단한 .dockerignore 파일을 생성한다.

# .dockerignore
node_modules
.next
dockerfiles
.eslint*
*.md

 

제일 간단하게 이미지에 포함하지 않아야 할 것들만 명시했다.

무거운 이미지를 만들기 위한 기본적인 Dockerfile 을 작성해보자.

# Dockerfile.basic
# Stage 1: Building the app
FROM node:18

# Set the working directory in the container
WORKDIR /app

# Copy package.json and package-lock.json (or yarn.lock) to workdir
COPY package.json yarn.lock ./

# Install dependencies
RUN yarn install --frozen-lockfile

# Copy the rest of your app's source code
COPY . .

# Build your Next.js app
RUN yarn build

EXPOSE 3000

CMD ["yarn", "start"]

 

node:18 베이스 이미지에서 yarn 으로 dependency 를 설치하고, yarn build 를 실행한 이후 서버를 실행하도록 했다. 이미지를 만들어서 실행해 보면, 웹페이지가 잘 불러와진다.

$ docker build -t next-basic -f Dockerfile.basic .
$ docker run --rm --name basic -p 3000:3000 next-basic        
yarn run v1.22.19
$ next start
   ▲ Next.js 14.1.0
   - Local:        http://localhost:3000

 ✓ Ready in 226ms

 

Next 페이지가 잘 보여진다

 

이미지의 사이즈를 확인해 보면 3GB 가 넘는 것을 확인할 수 있다.

$ docker images
REPOSITORY       TAG        IMAGE ID     CREATED                  SIZE
next-basic       latest                  About a minute ago       3.08GB

가벼운 이미지 만들기

Next.js 앱을 구동하는 보다 가벼운 이미지를 만들기 위해, next config 의 output 옵션의 standalone 모드를 사용할 수 있다. 대부분 최적화된 도커 이미지를 만들기 위해 multi-stage 빌드를 사용하곤 하는데, 이 경우에도 동일하게 standalone 빌드를 하고 그 결과물을 복사하는 방식으로 진행한다.

 

먼저 output 옵션을 변경하기 위해 next.config.mjs 파일을 수정한다.

# next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  output: 'standalone',
};

export default nextConfig;

 

output 옵션을 standalone 으로 변경하였다. standalone 모드에 대한 설명은 공식문서에서도 확인할 수 있다.

 

next.config.js Options: output | Next.js

Next.js automatically traces which files are needed by each page to allow for easy deployment of your application. Learn how it works here.

nextjs.org

이제, Dockerfile.standalone 파일을 작성한다.

# Dockerfile.standalone
# Stage 1: Building the app
FROM node:18-alpine AS builder

# Set the working directory in the container
WORKDIR /app

# Copy package.json and package-lock.json (or yarn.lock) to workdir
COPY package.json yarn.lock ./

# Install dependencies
RUN yarn install --frozen-lockfile

# Copy the rest of your app's source code
COPY . .

# Build your Next.js app
RUN yarn build

FROM node:18-alpine3.18

WORKDIR /app

# Copy necessary files needed for standalone next server
COPY --from=builder /app/.next/standalone ./standalone
COPY --from=builder /app/public ./standalone/public
COPY --from=builder /app/.next/static ./standalone/.next/static

EXPOSE 3000

CMD [ "node", "./standalone/server.js" ]

 

일부러 극적인 효과를 위해 베이스 이미지도 node:18-alpine 으로 변경하였다. builder 에서 standalone 아티팩트를 빌드하고, 그 결과물을 runner 로 복사한다. 이미지 생성 후 실행을 해보자.

$ docker build -t next-standalone -f Dockerfile.standalone .
$ docker run --rm --name basic -p 3000:3000 next-standalone        
   ▲ Next.js 14.1.0
   - Local:        http://3fdc18c890c9:3000
   - Network:      http://172.17.0.2:3000

 ✓ Ready in 55ms

 

 

여전히 웹 페이지가 잘 불러와진다.

여전히 잘 불러와지는지

 

이미지 사이즈를 비교해 보면 100MB대의 극적인 수치로 줄어든 것을 확인할 수 있다.

$ docker images
REPOSITORY          TAG         IMAGE ID      CREATED                  SIZE
next-standalone     latest                    About a minute ago       148MB
next-basic          latest                    9 minutes ago            3.08GB

결론

Kubernetes 와 같은 컨테이너 기반의 환경에서는 docker image 크기만 줄여도 배포 속도나 용량 등 많은 부분이 쾌적해진다. Next 외에도 다른 프레임워크를 사용한 서비스도 최적화 방법을 계속 연구할 예정이다.

Appendix

전체 코드는 Github Repo에서 확인할 수 있습니다.

 

GitHub - k2sebeom/next-docker-optimization: Optimized Docker Image for next app

Optimized Docker Image for next app. Contribute to k2sebeom/next-docker-optimization development by creating an account on GitHub.

github.com