Devlog.
게시일

Docker, NGINX, GitHub Actions로 Next.js 자동, 무중단 배포하기

cover
date
Jul 3, 2023
slug
docker-nginx-github-actions로-nextjs-자동-무중단-배포하기
status
Published
tags
Next.js
Docker
NGINX
Deploy
GitHub Actions
summary
Docker, NGINX, GitHub Actions로 Next.js 자동, 무중단 배포하기
type
Post

Docker 설치하기

Docker 공식 문서에 나온 방법대로 Ubuntu에 Docker를 설치한다.

폴더 구조

... ├── .dockerignore ├── infra │ ├── Dockerfile │ ├── docker-compose.yml │ ├── deploy.sh │ └── nginx │ └── default.conf ...

Docker 이미지 경량화

Docker 이미지의 크기를 줄이기 위해서 alpine 이미지와 multi-stage build와, Next.js의 standalone 빌드를 사용한다.
💡
alpine 이미지 Docker에 alpine이 포함된 이미지는 Alpine Linux 배포판을 기반으로 하는 경량 Docker 이미지를 말하며, 작은 크기와 최소한의 접근 방식으로 알려져 있어 리소스가 제한된 환경에 적합하다.
💡
Multi-stage build 빌드 프로세스를 런타임 환경에서 분리하고 불필요한 빌드 아티팩트를 제거하여 보다 효율적이고 안전한 Docker 이미지 생성을 가능하게 하는 기능
  • 기본, multi-stage build, multi-stage build with standalone 비교
    • create-next-app@latest 기준, node:18-alpine 이미지 사용
      create-next-app@latest 기준, node:18-alpine 이미지 사용
      multi-stage build with standalonenormal에 비해 크기가 1/3 이상 줄어든 것을 확인할 수 있다.
      비교에 사용한 Dockerfile
      normal
      FROM node:18-alpine WORKDIR /app COPY . . RUN npm install RUN npm run build EXPOSE 3000 ENV PORT 3000 CMD ["npm", "run", "start"]
      multi-stage
      FROM node:18-alpine AS base FROM base AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm install 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 COPY --from=builder /app/.next/ ./.next COPY --from=builder /app/package.json ./ COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/public ./public COPY --from=builder /app/next.config.js ./ EXPOSE 3000 ENV PORT 3000 CMD ["npm", "run", "start"]
      standalone
      FROM node:18-alpine AS base FROM base AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm install 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 COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public EXPOSE 3000 ENV PORT 3000 CMD ["node", "server.js"]

Next.js standalone 설정

next.config.js에서 다음과 같이 설정한다.
// next.config.js module.exports = { output: 'standalone', }

.dockerignore 파일 작성

배포 시 필요없는 파일들을 .dockerignore에 작성하여 이미지 크기를 줄이고 빌드 속도를 향상시키도록 한다.
# .dockerignore node_modules .next Dockerfile .dockerignore .git .gitignore *.md *.sh *.yml

Dockerfile 작성

# infra/Dockerfile FROM node:18-alpine AS base FROM base AS deps # alpine 이미지는 glibc 대신 musl libc을 사용. # 특정 라이브러리에 대해 문제가 발생할 수 있으므로 libc6-compat 패키지를 추가하는 것이 좋다. # https://github.com/nodejs/docker-node/tree/main#nodealpine RUN apk add --no-cache libc6-compat WORKDIR /app # 의존성 패키지 설치 COPY package.json package-lock.json ./ RUN npm install FROM base AS builder WORKDIR /app COPY . . # deps 단계에서 설치한 의존성 패키지 복사 COPY --from=deps /app/node_modules ./node_modules # build 진행 RUN npm run build FROM base AS runner WORKDIR /app # 보안 문제가 발생할 수 있으므로 도커 컨테이너 내에서 루트 권한으로 서버 프로세스를 실행하지 않는 것이 좋다. RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs # standalone 폴더 및 정적 파일 복사 COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/public ./public USER nextjs EXPOSE 3000 ENV PORT 3000 CMD ["node", "server.js"]

무중단 배포하기

무중단 배포를 위해 Blue, Green 배포 방식으로 배포한다.
💡
Blue, Green 배포 가동 중지 시간을 최소화하고 위험을 완화하며 소프트웨어 애플리케이션 및 인프라에 대한 원활한 업데이트를 달성하기 위한 전략. 두 개의 동일한 환경을 보유하고 전략적으로 트래픽을 전환함으로써 프로세스는 지속적인 가용성을 보장하는 동시에 개발자가 새로운 변경 사항을 전체 사용자 기반에 공개하기 전에 유효성을 검사할 수 있도록 한다.

docker-compose.yml 작성

# infra/docker-compose.yml version: '3' services: nginx: image: nginx:1.25.0-alpine container_name: nginx restart: always ports: - '80:80' - '443:443' volumes: - /etc/letsencrypt/:/etc/letsencrypt/ - ./logs/nginx/:/var/log/nginx/ - ./nginx/:/etc/nginx/conf.d/ environment: - TZ=Asia/Seoul blue: image: ${DOCKER_REGISTRY}/${DOCKER_APP_NAME}:${DOCKER_IMAGE_TAG} container_name: ${DOCKER_APP_NAME}-blue restart: always expose: - 3000 environment: - TZ=Asia/Seoul green: image: ${DOCKER_REGISTRY}/${DOCKER_APP_NAME}:${DOCKER_IMAGE_TAG} container_name: ${DOCKER_APP_NAME}-green restart: always expose: - 3000 environment: - TZ=Asia/Seoul
이미지 태그 등을 환경변수를 통해 받도록 하고 GitHub Actions에서 배포할 때 이를 변경하여 rollback이 용이하도록 작성한다.

nginx 설정 파일 작성

# nginx/default.conf upstream nextjs { server blue:3000; } server{ server_name <도메인>; location / { proxy_pass http://nextjs; } listen [::]:443 ssl; # managed by Certbot listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/<도메인>/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/<도메인>/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server{ if ($host = <도메인>) { return 301 https://$host$request_uri; } # managed by Certbot listen [::]:80; listen 80; server_name <도메인>; return 404; # managed by Certbot }

Docker certbot으로 SSL 인증서 받기

sudo docker run -it --rm --name certbot \ -v '/etc/letsencrypt:/etc/letsencrypt' \ -v '/var/lib/letsencrypt:/var/lib/letsencrypt' \ certbot/certbot certonly -d '도메인' --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory

deploy.sh 작성

#!/bin/bash IS_BLUE_UP=$(docker ps | grep ${DOCKER_APP_NAME}-blue) docker-compose up -d nginx if [ "$IS_BLUE_UP" ]; then echo "Blue is up, deploying green" docker-compose pull green docker-compose up -d green while [ 1 = 1 ]; do sleep 2 REQUEST=$(docker exec nginx curl http://green:3000) if [ -n "$REQUEST" ]; then echo "Green is up" break; fi done; sed -i 's/blue/green/g' nginx/default.conf echo "Reload nginx" docker exec nginx nginx -s reload echo "Stop blue" docker-compose stop blue else echo "Green is up, deploying blue" docker-compose pull blue docker-compose up -d blue while [ 1 = 1 ]; do sleep 2 REQUEST=$(docker exec nginx curl http://blue:3000) if [ -n "$REQUEST" ]; then echo "Blue is up" break; fi done; sed -i 's/green/blue/g' nginx/default.conf echo "Reload nginx" docker exec nginx nginx -s reload echo "Stop green" docker-compose stop green fi echo "Deploy finished"

초기 컨테이너 띄우기

nginx 설정 파일에 작성한대로 blue 컨테이너를 먼저 띄워 놓는다.
DOCKER_REGISTRY=레지스트리 DOCKER_APP_NAME=앱이름 DOCKER_IMAGE_TAG=태그 docker-compose up -d blue
docker-compose.yml에서 환경변수로 이미지 이름을 받기 때문에 환경 변수를 설정해주고 docker-compose 명령을 실행한다.

GitHub Actions workflow 작성

name: deploy on: push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx # 멀티플랫폼 이미지 빌드, 캐시 내보내기 등을 위해 사용. uses: docker/setup-buildx-action@v2 - name: Login to Docker Hub # Docker Hub 로그인을 위해 사용 uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - name: Build and push production # Docker 이미지를 빌드하고 푸시할 수 있는 GitHub action uses: docker/build-push-action@v4 with: context: . file: ${{ vars.DOCKERFILE_PATH }} push: true # rollback 용도로 버전이 명시된 태그를 추가로 push한다. tags: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_APP_NAME }}:latest,${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_APP_NAME }}:${{ env.VERSION }} # GitHub Actions 캐시에 직접 빌드 캐시를 저장한다. (experimental) # https://docs.docker.com/build/ci/github-actions/cache/#github-cache cache-from: type=gha cache-to: type=gha,mode=max # 빌드 대상 플랫폼 설정 platforms: ${{ vars.DOCKER_PLATFORMS }} - name: Deploy production # ssh를 통해 배포할 서버로 접속하여 배포 스크립트를 실행하기 위해 사용한다. uses: appleboy/ssh-action@master env: DOCKER_REGISTRY: ${{ secrets.DOCKER_USERNAME }} DOCKER_IMAGE_TAG: latest DOCKER_APP_NAME: ${{ secrets.DOCKER_APP_NAME }} with: host: ${{ secrets.SSH_HOST }} username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} envs: DOCKER_REGISTRY,DOCKER_IMAGE_TAG,DOCKER_APP_NAME script: | cd ${{ vars.DEPLOY_PATH }} ./deploy.sh
GitHub 저장소 → SettingsSecrets and variabesActions에서 변수를 설정한다.
 
이후 main 브랜치로 push 될때마다 빌드와 배포가 자동으로 이루어지게 된다.

Reference