Next.js 프로젝트에 Docker 적용기

MochaChoco·2023년 8월 28일
1
post-thumbnail

배포를 위해서 개발중인 Next.js 프로젝트에 Docker를 적용해야 했다. 여태 Docker는 백엔드 개발자가 별도로 세팅해서 우리에게 전달준 것을 사용한게 전부였지만 이번 프로젝트는 공부하는 김에 내가 한번 직접 세팅해보기로 했다.

Docker란?

DockerContainer를 이용하여 Application을 신속하게 구축, 테스트 및 배포할 수 있는 소프트웨어 플랫폼이다. 여기서 말하는 Container란 표준화되고 실행 가능한 구성요소이며
애플리케이션의 소스 코드 및 Application이 동작하는 운영 체제(OS) 및 라이브러리, 종속 항목(Dependancy) 등을 조합한 것을 말한다.

따라서 Docker를 사용하면 Docker로 Container화 가능한 모든 환경에서 보다 쉽게 프로젝트를 배포할 수 있다는 장점이 있다.

※ Virtual Machine과의 차이점
Docker는 Virtual Machine와 마찬가지로 리소스를 별도로 구분해서 관리한다는 특징이 있지만 Virtual Machine은 OS 단위로 리소스를 구분하는 반면, Container는 Application 단위로 구분한다.

docker build 명령어를 사용하면 Image 파일이 생성되는데, 이후에 docker run 명령어를 통해 앞서 생성된 Image 파일을 기반으로 Container가 생성된다. 이처럼 Image와 Container는 1:N 관계이다. 보다 쉽게 설명하자면 붕어빵 틀(Image)과 붕어빵(Container) 같은 관계라고 할 수 있다.

Dockerfile 작성 방법

Dockerfile은 프로젝트 root폴더에 작성한다. 아래의 코드 블록은 Vercel 공식 repo에 있는 Next.js용 Dockerfile을 기반으로 작성한 것이이며, npm 대신 yarn을 사용한다는 전제를 기반으로 한다.

# Multi-stage build

# 1단계: 환경 설정 및 dependancy 설치
FROM node:18-alpine AS deps
RUN apk add --no-cache libc6-compat

# 명령어를 실행할 디렉터리 지정
WORKDIR /usr/src/app

# Dependancy install을 위해 package.json, package-lock.json, yarn.lock 복사 
COPY package.json yarn.lock ./ 

# Dependancy 설치 (새로운 lock 파일 수정 또는 생성 방지)
RUN yarn --frozen-lockfile 

###########################################################

# 2단계: next.js 빌드 단계
FROM node:18-alpine AS builder

# Docker를 build할때 개발 모드 구분용 환경 변수를 명시함
ARG ENV_MODE 

# 명령어를 실행할 디렉터리 지정
WORKDIR /usr/src/app

# node_modules 등의 dependancy를 복사함.
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY . .

# 구축 환경에 따라 env 변수를 다르게 가져가야 하는 경우 환경 변수를 이용해서 env를 구분해준다.
COPY .env.$ENV_MODE ./.env.production
RUN yarn build

###########################################################

# 3단계: next.js 실행 단계
FROM node:18-alpine AS runner

# 명령어를 실행할 디렉터리 지정
WORKDIR /usr/src/app
 
# container 환경에 시스템 사용자를 추가함
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# next.config.js에서 output을 standalone으로 설정하면 
# 빌드에 필요한 최소한의 파일만 ./next/standalone로 출력이 된다.
# standalone 결과물에는 public 폴더와 static 폴더 내용은 포함되지 않으므로, 따로 복사를 해준다.
COPY --from=builder /usr/src/app/public ./public
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next/static ./.next/static

# 컨테이너의 수신 대기 포트를 3000으로 설정
EXPOSE 3000

# node로 애플리케이션 실행
CMD ["node", "server.js"] 

# standalone으로 나온 결과값은 node 자체적으로만 실행 가능
# CMD ["npm", "start"]

위의 코드는 Multi-stage build 방식을 사용하고 있다. Multi-stage build 방식은 Docker 17.05 버전에 도입된 기능으로 이미지 구축을 위한 여러 단계를 한 Dockerfile 내에 정의하는 방식을 말한다.
여기서는 종속성 설치, Next.js 빌드, Next.js 실행 부분 3단계로 나누어 Dockerfile을 작성하였다.

위의 DockerFile 예제는 실행에 필요한 몇몇 파일과 경량화된 Next.js의 빌드 결과물만 이미지에 반영된다.

Multi-Stage Build의 장점

  1. Build 과정을 더 작은 단계로 나누기 때문에 최종 Image에 필요하지 않은 불필요한 파일들을 제거하기 용이하다. 따라서 Image의 Size가 줄어들어 배포 시간이 단축되고 저장소의 비용을 절감할 수 있다.
  1. 캐싱 기능을 지원하므로 소스 코드나 종속성이 변경되지 않은 경우 이를 재사용할 수 있다. 그러므로 빌드 속도가 빨라지고 개발 주기가 단축될 수 있다.

1. 환경 설정 및 dependancy 설치

# 1단계: 환경 설정 및 dependancy 설치
FROM node:18-alpine AS deps
RUN apk add --no-cache libc6-compat

# 명령어를 실행할 디렉터리 지정
WORKDIR /usr/src/app

# Dependancy install을 위해 package.json, package-lock.json, yarn.lock 복사 
COPY package.json yarn.lock ./ 

# Dependancy 설치 (새로운 lock 파일 수정 또는 생성 방지)
RUN yarn --frozen-lockfile 

FROM 명령으로 사용할 이미지를 node:18-alpine로 지정하고 AS 키워드로 현재 단계의 이름을 deps로 지정해준다. 그리고 RUN 키워드로 alpine의 패키지 매니저를 통해 libc6-compat를 설치한다. node:18-alpine는 경량화된 라이브러리이므로 필요한 구성요소가 누락될 수도 있는데, 이러한 점 때문에 호스트 시스템에 따라 process.dlopen 명령을 실행할때 에러가 발생할 수 있다. 이러한 사항을 방지하기 위해 libc6-compat 라이브러리를 추가적으로 설치하도록 한다.

WORKDIR 명령을 사용하여 내부의 작업 디렉터리를 /usr/src/app로 설정하고, COPY 명령을 사용하여 호스트 시스템에서 이미지로 package.json, yarn.lock 파일을 복사한다.

이후, Yarn으로 종속성(Dependancy)를 설치하는데, 별도의 lock 파일의 생성을 막기위해 인자로 --frozen-lockfile를 넣어준다.

2. next.js 빌드 단계

# 2단계: next.js 빌드 단계
FROM node:18-alpine AS builder

# Docker를 build할때 개발 모드 구분용 환경 변수를 명시함
ARG ENV_MODE

# 명령어를 실행할 디렉터리 지정
WORKDIR /usr/src/app

# node_modules 등의 dependancy를 복사함.
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY . .

# 구축 환경에 따라 env 변수를 다르게 가져가야 하는 경우 환경 변수를 이용해서 env를 구분해준다.
COPY .env.$ENV_MODE ./.env.production
RUN yarn build

1단계와 마찬가지로 FROM 명령으로 사용할 이미지를 node:18-alpine로 지정하고 AS 키워드로 현재 단계의 이름을 builder로 지정해준다. 그 후 ARG 명령어로 환경 변수(ENV_MODE)를 정의해주고 WORKDIR 명령어로 현재 위치를 /usr/src/app로 지정준다. 환경 변수는 아래에 서술할 빌드 명령어에 인자(development 또는 production)로 넣을때 사용된다.

COPY 명령어로 1단계(deps)에서 종속성 설치 후에 생성된 node_modules 폴더와 기타 빌드에 필요한 모든 파일들을 이미지로 복사한다.
env 파일도 이때 같이 복사되는데 package.json에 있는 script를 사용하는 것이 아니라 yarn으로만 빌드되기에, 빌드 결과물은 무조건 .env.production 파일을 참조한다는 문제점이 있다. 따라서 구축 환경에 따라 env 변수를 다르게 가져가야 할 경우 해당 환경에 사용되는 env 파일을 .env.production에 덮어씌우는 방식으로 해결하게끔 설정했다.

env 파일까지 복사했으면 Run 커맨드로 Next.js 결과물을 build한다.

3. next.js 실행 단계

# 3단계:  next.js 실행 단계
FROM node:18-alpine AS runner

# 명령어를 실행할 디렉터리 지정
WORKDIR /usr/src/app
 
# container 환경에 시스템 사용자를 추가함
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# next.config.js에서 output을 standalone으로 설정하면 
# 빌드에 필요한 최소한의 파일만 ./next/standalone로 출력이 된다.
# standalone 결과물에는 public 폴더와 static 폴더 내용은 포함되지 않으므로, 따로 복사를 해준다.
COPY --from=builder /usr/src/app/public ./public
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /usr/src/app/.next/static ./.next/static
 
# 컨테이너의 수신 대기 포트를 3000으로 설정
EXPOSE 3000

# node로 애플리케이션 실행
CMD ["node", "server.js"] 

# standalone으로 나온 결과값은 node 자체적으로만 실행 가능
# CMD ["npm", "start"]

1, 2단계와 마찬가지로 FROM 명령으로 사용할 이미지를 node:18-alpine로 지정하고 AS 키워드로 현재 단계의 이름을 runner로 지정해준다. 그 후 WORKDIR 명령어로 현재 위치를 /usr/src/app로 지정준다음 컨테이너 환경에 시스템 사용자를 추가한다.

Docker Image 용량을 줄이기 위해선 Next.js 빌드 결과물 용량을 줄여야 하는데, 마침 Next.js 공식 문서에서 아래와 같은 방법을 소개하고 있다.

// next.config.js
const nextConfig = {
  output: "standalone",
};

module.exports = nextConfig;

설정 방법은 간단하다. next.config.js 파일에 output을 standalone으로 설정해주면 끝이다. 다만 이렇게 하면 정적인 이미지나 폰트 등 public한 asset들은 결과물에 포함되지 않기에 별도로 복사해주는 과정이 필요하다. 따라서 COPY 명령어로 standalone 폴더 뿐만 아니라 프로젝트의 public 폴더들도 함께 복사해준다.

EXPOSE 명령어로 수신 대기 포트를 설정하고, 실행 명령어를 정의하면 Dockerfile 작성이 완료된다.

빌드 및 실행 테스트

1. 이미지 빌드

  • 개발환경: docker build -t {{ 빌드할 Image 파일명 }} -f ./Dockerfile . --build-arg ENV_MODE=development
  • 운영환경: docker build -t {{ 빌드할 Image 파일명 }} -f ./Dockerfile . --build-arg ENV_MODE=production
    ex) docker build -t docker-test -f ./Dockerfile . --build-arg ENV_MODE=development

앞서 설명했지만 이미지 빌드는 docker build 명령어를 사용하여 Dockerfile을 기반으로 Image가 생성하는 단계이다.

이번 Next.js 프로젝트는 개발 환경(development), 운영 환경(production)에 따라 사용되는 API 키 등이 별도로 .env 파일로 관리되고 있었다. 따라서 배포 환경에 따라 사용되는 env파일도 달라지므로 Docker 빌드 명령어도 구분이 되어야 한다.

2. 이미지 실행

  • docker run -it --rm -p 3000:3000 {{ 빌드한 Image 파일명 }}
    ex) docker run -it --rm -p 3000:3000 docker-test

이미지 실행은 docker run 명령어를 사용하여 앞서 빌드한 Image파일을 기반으로 Container를 생성하는 단계이다. Container는 생성됨과 동시에 지정된 port에 바인딩된다.

위에서 빌드한 Image 파일명을 입력하고 localhost:3000으로 들어가보면 docker로 build한 Next.js 결과물이 잘 출력되는 것을 볼 수 있다.

샘플코드

github 저장소 이동 (https://github.com/MochaChoco/docker-test)

참고자료

Docker의 개념 및 핵심 설명
Next.js 프로젝트 docker 배포 + 이미지 크기 줄이기
Vercel - Dockerfile 작성 예시
Dockerfile 기본 명령어 정리
Docker 다단계 빌드 이해

profile
길고 가늘게

2개의 댓글

comment-user-thumbnail
2024년 6월 5일

포스트 잘봤습니다~ 마지막 1. 예시에 ARG_MODE (X) ENV_MODE인것 같습니다!

1개의 답글