Docker에 대해 공부하면서 이전에 만들었던 Next.js 프로젝트에 Docker를 적용해 보았다.
1. Docker로 이미지 배포 및 컨테이너 실행
프로젝트 root에 Dockerfile 작성
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
ENTRYPOINT ["npm", "run", "start"]
.dockerignore 작성
node_modules
.next
.github
.gitignore
.env* # 환경 변수가 빌드 타임에 사용된다면 제거
.vscode
README.md
docker-compose.yaml 작성
services:
next-app:
container_name: "next-app"
build: .
ports:
- 3000:3000
env_file:
- .env
Docker Compose로 컨테이너 생성 및 실행
docker compose up -d --build
이미지가 생성되고, 컨테이너가 실행되고 있는 걸 확인할 수 있다.
localhost로 접속도 가능하다.
2. Multi-stage builds 적용
다른 사람들은 어떻게 Docker 파일을 설정했나 궁금해서 Next.js에 Docker를 설정하는 글을 찾아보다가 Next.js 공식 문서에서 Dockerfile의 예시를 제공하고 있는 것을 발견했다. 예시에서는 내가 처음 작성했던 것보다 훨씬 더 많은 명령어들이 작성되어 있어서 무슨 차이가 있는지 알아보았다.
공식 문서의 Dockerfile 예시
# syntax=docker.io/docker/dockerfile:1
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
공식 문서의 Dockerfile은 Multi-stage builds(멀티 스테이지 빌드) 방식을 사용하고 있다. Multi-stage builds는 Dockerfile 내에서 여러 단계를 거쳐 빌드하는 방법으로, 이를 통해 빌드용 이미지와 실행용 이미지를 분리할 수 있다.
위 Dockerfile은 세 단계로 나누어진다.
베이스 이미지 설정
FROM node:18-alpine AS base
베이스 이미지로 node 18 버전의 alpine을 사용한다. alpine은 Alphine-Linux를 기반으로 한 이미지로, 일반 이미지보다 용량이 훨씬 작아 Docker에서 자주 사용된다. AS base를 사용하면 베이스 이미지를 여러 단계에서 재사용할 수 있다.
1. 의존성 설치
FROM base AS deps
# libc6-compat 추가
RUN apk add --no-cache libc6-compat
# 명령어를 실행할 디렉터리 지정
WORKDIR /app
# 사용하고 있는 패키지 매니저에 따라 의존성 설치
# 의존성과 관련된 파일 복사
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
# 의존성 설치
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
docker-node의 문서에 의하면, process.dlopen와 같은 일부 모듈이 정상적으로 작동하려면 이미지에 libc6-compat을 추가해야 한다고 한다.
2. 프로젝트 빌드
FROM base AS builder
WORKDIR /app
# 이전 stage인 deps에서 설치된 node_modules를 현재 builder stage로 복사
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js는 일반적인 사용에 대한 익명의 텔레메트리 데이터를 수집한다.
# 데이터 제공을 원하지 않으면 아래 환경 변수를 설정한다.
# ENV NEXT_TELEMETRY_DISABLED=1
# 프로젝트 빌드
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
빌드 단계에서는 개발 소스 코드를 최적화된 프로덕션 코드로 변환한다.
3. 프로젝트 실행
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# 런타임 중 텔레메트리를 비활성화하려면 아래 환경 변수를 설정
# ENV NEXT_TELEMETRY_DISABLED=1
# 컨테이너 환경에 시스템 사용자 추가
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# builder stage에서 만들어진 /app/public 디렉토리를 현재 runner stage로 복사
COPY --from=builder /app/public ./public
# 출력 파일을 추적하여 이미지 용량을 줄인다. (standalone 사용)
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
# 정적 파일 복사
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# root 사용자 대신, 위에서 만든 nextjs 사용자로 앱을 실행
USER nextjs
EXPOSE 3000
ENV PORT=3000
# next build의 standalone output에서 server.js가 생성됨
ENV HOSTNAME="0.0.0.0"
# 컨테이너가 시작되면 실행
CMD ["node", "server.js"]
Next.js 공식 문서에 따르면, 이전에는 Next.js를 Docker로 배포할 때 next start를 실행하기 위해 패키지의 dependencies에 있는 모든 파일을 설치해야 했지만, Next.js 12부터는 .next/ 디렉터리의 Output File Tracing을 활용하여 필요한 파일만 포함할 수 있게 됐다.
Next.js는 node_modules의 선택된 파일을 비롯하여 프로덕션 배포에 필요한 파일만 복사하는 standalone 폴더를 자동으로 생성할 수 있다. next.config.js에서 이 자동 복사 기능을 활성화할 수 있다.
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
...
}
module.exports = nextConfig
또한 이를 통해 최소한의 서버 파일인 server.js 파일이 생성되면서 next start 대신 node server.js로 프로젝트를 실행한다.
예시를 참고하여 내 프로젝트 환경에 맞게 수정한 Dockerfile
FROM node:20-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
수정된 Dockerfile을 이용한 이미지 생성 결과
860MB에서 175MB로 이미지 용량이 확 줄어들었다!
Docker는 이전에 팀 프로젝트에서 백엔드 환경을 실행할 때만 사용해 봤었는데 프론트엔드에서 적용해 보니 새로웠다. 또 이전에는 단순히 백엔드 팀원이 세팅해 놓은 것을 docker compose로 실행만 했었는데 직접 Docker 설정을 해볼 수 있어서 좋았다! 이번엔 간단하게 해봤지만 이후에 다른 컨테이너 추가나 aws에 배포하는 것도 해보고 싶다.
참고 자료
next.js/examples/with-docker/Dockerfile at canary · vercel/next.js
The React Framework. Contribute to vercel/next.js development by creating an account on GitHub.
github.com
Next.js 프로젝트 docker 배포 + 이미지 크기 줄이기
이번에 Docker를 이용하여 전체 프로젝트의 배포 설정을 구현하기로 결정했다.현재 프로젝트의 구성은 server / client 두 개의 레포로 나뉘어져 있어서, 각각 Dockerfile을 구성하기로 했다.Docker로 배포
velog.io
잘못된 부분이 있다면 댓글로 알려주시면 감사하겠습니다!
'Develop > NextJS' 카테고리의 다른 글
[Next.js] Suspense를 이용한 Skeleton UI 적용하기 (0) | 2024.08.19 |
---|---|
[Next.js] SEO 최적화 & 구글/네이버에 사이트 등록하기 (0) | 2024.08.10 |
[Next.js] 스크롤 최상단 이동 버튼 구현하기 (0) | 2024.08.05 |
[Next.js] 서버 & 클라이언트 컴포넌트에서 토큰 관리하기 (0) | 2024.07.24 |
[Next.js] Data fetching과 Caching Mechanism (0) | 2024.06.12 |