0ju-log
💻 Frontend

[Docker] (도커와의) 첫 만남은 너무 어려워

💡 시작하기

냅다 Docker 빌드와 CI/CD 구축을 하게 된 나..

게다가 패키지 매니저도 yarn classic에서 yarn berry로 마이그레이션해야 해서, 기존에 쓰던 Dockerfile과는 조금 다르게 진행해야 한다고 한다.

하지만 난 Docker 빌드와 CI/CD 구축이 모두 처음인걸…?

그래서 Docker build를 수행할 수 있는 Dockerfile에 대해 자세히 알아보고자 합니당

🐳 Dockerfile 작성하기

Dockerfile을 작성하기 전, 알아두어야 할 것이 있다. 바로 빌드 과정!

빌드 과정은 총 3개의 스테이지를 거친다

  • stage 1 : deps 단계

    FROM base AS deps

  • stage 2: builder 단계

    FROM base AS builder

  • stage 3 : runner 단계

    FROM base AS runner

1️⃣ Stage 1 : deps 단계

📖 패키지 설치 전용 단계

  • package.json / yarn.lock 기반으로 .yarn/cache와 .pnp.cjs를 만든다

yarn berry 마이그레이션으로 인해 기존 Dockerfile에서 수정된 부분

# package.json, yarn.lock, .yarnrc.yml을 복사한다
# 패키지가 변경되지 않으면 해당 단계는 캐시된다 -> 속도 향상됨
COPY package.json yarn.lock .yarnrc.yml ./


# yarn berry의 경우, .yarn 폴더에 패키지가 ZIP 파일로 저장되어있으므로 해당 코드가 필요하다
# COPY {source} {destination} 

# ./.yarn -> /app/.yarn


COPY .yarn ./.yarn

# corepack으로 yarn을 활성화하고, berry로 설정 후, immutable로 yarn install을 진행한다
# --immutable = yarn.lock이 조금이라도 다르면 오류 → Docker/CI에서 필수
RUN corepack enable
RUN yarn set version berry
RUN yarn install --immutable
  • 해당 단계가 끝나면
    • .pnp.cjs 생성
    • .yarn/cache/*.zip 생성

2️⃣ Stage 2: builder 단계

📖 앱을 빌드하는 단계

  • deps 단계에서 만든 의존성 + 소스코드를 합쳐 next 빌드를 수행한다

→ .next/standalone, .next/static 이 생성된다

yarn berry 마이그레이션으로 인해 기존 Dockerfile에서 수정된 부분

# deps 단계에서 만든 여러 PnP파일들을 복사한다
COPY --from=deps /app/.pnp.cjs /app/.pnp.cjs
COPY --from=deps /app/.yarnrc.yml ./.yarnrc.yml
COPY --from=deps /app/.yarn ./.yarn
COPY --from=deps /app/package.json ./package.json
COPY --from=deps /app/yarn.lock /app/yarn.lock

# 앱의 소스코드 전체를 복사한다
COPY . .

# Next.js 빌드 과정에서 패키지를 찾을 때 node_modules 대신 PnP 방식을 사용해야 하므로, 
# NODE_OPTIONS로 .pnp.cjs를 강제 로드한다.
ENV NODE_OPTIONS="--require=/app/.pnp.cjs"

# 이 단계가 deps, builder에 둘 다 있는 이유는
# builder 단계에서는 .pnp.cjs를 소스코드와 연결하는 작업이기 때문에 빠르게 진행된다
RUN yarn install --immutable

# Next.js 빌드를 수행한다
RUN yarn run build

3️⃣ Stage 3 : runner 단계

📖 실행 단계

  • builder 단계에서 나온 빌드 결과만 가져온다

→ next.js app을 noder server.js로 실행한다

runner 단계에서의 코드

# production 환경 설정
ENV NODE_ENV=production

# 새로운 시스템 그룹과 사용자 생성
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# builder 단계에서 public 폴더 복사
COPY --from=builder /app/public ./public

# prerender cache를 위한 .next 디렉토리 생성 및 권한 설정
RUN mkdir .next
RUN chown nextjs:nodejs .next

# 빌드 결과물 복사
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# 애플리케이션을 nextjs 사용자로 실행
USER nextjs

# 컨테이너 포트 설정
EXPOSE 3000
ENV PORT=3000

# 서버 실행
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

🐳 Docker build 실행하기

  1. CLI를 통해 aws에 로그인을 한다

  2. 도커에서 빌드를 시작한다

    • 도커 빌드 시, M1의 경우 이미지를 amd64로 만들어야 하는 경우가 있기에 --platform linux/amd64 를 꼭 추가해야 하며, 만약 그 외라면 해당 부분은 생략해도 된다!

      docker build --platform linux/amd64 -t {이미지 이름:이미지버전} {Dockerfile의 경로}

  3. 생성된 이미지에 태그를 붙인다

    docker tag {이미지 이름} {이미지 링크}

  4. 도커에 올린다

    docker push {링크}

  5. 생성된 이미지를 확인한다 : docker images

example

docker build --platform linux/amd64 -t my-next-app:1.0 .
docker tag my-next-app:1.0 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/my-next-app:1.0
docker push 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/my-next-app:1.0

📒 정리

yarn berry가 생각보다 신경쓸 만한 게 많아서 빌드 실패를 어마어마하게 많이 했다 흑흑

그래도 성공하니까 굉장히 뿌듯했다 희희..

수많은 나의 빌드 성공신화 과정.. 다 도커 빌드 실패해서 난 에러…
수많은 나의 빌드 성공신화 과정.. 다 도커 빌드 실패해서 난 에러…

근데 생각해보면 걍 로컬에서 빌드해보면 될 것을 왜 굳이 깃허브에 올려서 테스트를 했었는지 모르겠다 레전드 바보

다음 글에서는 이 과정을 바탕으로, Github Action에서 도커 빌드 → ECR 이미지 업로드 → ECS Task Definition까지 자동화하는 과정을 정리해볼 예정이다!

🔗 참고