node 기반의 앱이 항상 겪어야 하는 문제가 있다. 바로 node_modules 라는 짐을 지고 가야 한다는 것이다. Next.js 로 작성된 웹 페이지는 server side rendering 을 포함하기 때문에 React 처럼 static page 로 export 해서 가볍게 배포할 수가 없다. node_modules 의 기막힌 무거움을 해결하기 위해 pnpm 이나 yarn berry 같은 해결책이 나왔지만, docker image 로 빌드해서 배포하는 경우, 컨테이너 안에 node dependency 를 필연적으로 들고 있어야 해서 image 의 크기가 커지고 만다.
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
이미지의 사이즈를 확인해 보면 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
'개발 일지' 카테고리의 다른 글
[k8s] Kafka Connect 를 이용해서 MQTT 메세지를 Kafka Broker 로 Produce 하기 (0) | 2024.02.13 |
---|---|
[k8s] MQTT Broker Cluster 와 Kafka Cluster 를 이용한 Scalable 아키텍쳐 구성 (0) | 2024.01.25 |
[k8s] minikube 로 k8s 클러스터 실습하기 (0) | 2024.01.12 |
[Windows] NSIS 를 이용해서 Forms Application 배포하기 (0) | 2024.01.08 |
[Windows] NSIS 를 이용해서 설치 파일 패키징하기 (0) | 2024.01.08 |