NEXT CI/CD (feat. NEXT, nginx, docker-compose)

하니·2025년 2월 19일

인프라

목록 보기
8/9

폴더 구조

my-nextjs-project/
├── docker/
│   └── nginx/
│       └── default.conf  # Nginx 설정
├── .github/
│   └── workflows/
│       └── deploy.yml    # GitHub Actions 배포 설정
├── Dockerfile           # Next.js 앱 도커 이미지 설정
├── docker-compose.yml   # 컨테이너 구성 설정
├── .dockerignore       # 도커 빌드 제외 파일
├── next.config.js      # Next.js 설정
└── src/                # 소스 코드
  • 배포 흐름 : local -> github -> github actions -> ec2 -> docker containersNginx + Next.js
  1. EC2 서버에서 github actions를 통해 들어온 코드를 실행할 때 docker-compose가 실행된다.
  2. 사용자가 웹사이트 접속 → 요청이 서버의 80포트로 들어옴 → Nginx(80)가 해당 요청을 받음 → Nginx는 설정proxy_pass에 따라 요청을 Next.js 컨테이너의 3000포트로 전달 → Next.js(3000)

💡 작성한 파일들의 작동 순서와 방식
1. github main 브랜치에 코드 푸시
2. github/workflows/deploy.yml - github actions가 이 파일을 읽고 배포 과정을 시작한다. (docker compose 설치, 환경변수파일.env 생성, docker hub 로그인)
3. Dockerfile - github actions가 이 파일을 사용해 Next.js 앱 이미지 빌드

  • builder 단계 : Node.js 환경에서 Next.js앱을 빌드
  • runner 단계 : 빌드된 결과물만 가져와 실행 환경 구성
    (빌드된 이미지는 docker hub에 푸시)
  1. github actions가 다음 파일들을 EC2로 전송 docker-compose.yml docker/nginx/default.conf .env
  2. EC2서버에서 배포 실행
    docker hub 로그인 > 명령어: 이미지 가져오기-기존 컨테이너 중지-새 컨테이너 시작
  3. docker-compose.yml - EC2서버에서 이 파일을 읽고 컨테이너 설정
  • nginx 서비스: 80/443 포트를 사용하며 외부 요청을 받음
  • nextjs 서비스: docker hub에서 가져온 이미지를 실행하며 3000 포트를 사용
    (두 서비스는 app_network로 연결)
  1. docker/nginx/default.conf - nginx 설정 파일
    어떤 요청을 어디로 전달할지 정의, SSL 인증서 설정, 해당 파일은 docker-compose.yml에서 볼륨으로 마운트됨

next.config.js

import { withSentryConfig } from "@sentry/nextjs";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  output: "standalone", // Docker 컨테이너에서 Next.js 애플리케이션을 실행하기 위한 설정
  experimental: {
    turbo: {
      rules: {
        "*.svg": {
          loaders: ["@svgr/webpack"],
          as: "*.js",
        },
      },
    },
  },
  webpack: (config) => {
    // Add rule for SVG files
    config.module.rules.push({
      test: /\.svg$/,
      use: ["@svgr/webpack", "url-loader"],
    });

    return config;
  },
  reactStrictMode: true,
};

export default withSentryConfig(nextConfig, {
  // For all available options, see:
  ... sentry 설정

.dockerignore

node_modules
.next
.git
.env*

docker/nginx/default.conf

# 프론트엔드용 서버 블록
server {
    listen       80; # IPv4
    listen       [::]:80; # IPv6
    # server_name mywareho.me;

    location / {
        proxy_pass http://next-app:3000; # docker-compose 서비스명과 일치
        # Next.js의 WebSocket 기능 지원 설정 (+ hot reload 기능)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        # 프록시 헤더 설정
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        # 성능 최적화 위한 gzip 압축
        # gzip on;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    }
}

# 백엔드용 서버 블록

Dockerfile

node.js 공식, Node.js 릴리즈 일정표, Node.js 공식 Docker 이미지 정보 를 확인해보면 Node.js 22가 LTS 버전인 것을 확인할 수 있다.

# 빌드
# nodejs의 버전을 명시해주고, builder라는 별칭을 부여
FROM node:22-alpine3.21 AS builder
# work 디렉토리를 /app으로 설정
WORKDIR /app
# 패키지 파일 복사 및 설치
COPY package*.json package-lock.json ./
# npm 설치
RUN npm install
# 프로덕션 종속성만 설치
# RUN npm ci --only=production
COPY . .
# Next.js 빌드
RUN npm run build

# 실행
FROM node:22-alpine3.21 AS runner
WORKDIR /app
ENV NODE_ENV=production
# 필요한 파일만 복사
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/server ./server
# Sentry source maps
COPY --from=builder /app/.next/server ./server
COPY --from=builder /app/.next/static ./.next/static 

# 오픈할 포트 설정
EXPOSE 3000
CMD ["node", "server.js"]

docker-compose.yml

version: "3"
services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80" # 호스트의 80포트를 컨테이너의 80포트와 매핑
    volumes:
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf # Nginx 설정 파일 마운트
    depends_on: # nextjs 서비스가 먼저 실행되어야 함
      - nextjs
    restart: always # 컨테이너 중단시 항상 재시작

  nextjs:
    build:
      context: .
      dockerfile: Dockerfile
    expose:
      - "3000" # 컨테이너 내부에서만 3000포트 접근 가능
    restart: always

deploy.yml

name: Deploy to EC2

# 워크플로우 트리거 조건 설정
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      # Sentry 환경 변수 설정
      - name: Create Sentry environment file
        run: |
          echo "NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}" >> .env
          echo "SENTRY_ORG=jinii9" >> .env
          echo "SENTRY_PROJECT=wms" >> .env

      # EC2로 파일 복사
      - name: Copy files to EC2
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          source: ".,!node_modules,!.next,!.git"
          target: "/home/ubuntu/app"

      # EC2에서 Docker Compose 실행
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            cd /home/ubuntu/app
            docker-compose down --rmi all --volumes 
            docker system prune -af                 
            docker-compose up --build -d
name: Deploy to EC2

# 워크플로우 트리거 조건 설정
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      # 환경 변수 파일 생성
      - name: Create environment file
        run: |
          # API URL (프로덕션용)
          echo "NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}" >> .env

          # Auth Secret
          echo "AUTH_SECRET=${{ secrets.AUTH_SECRET }}" >> .env

          # API Mocking 설정
          echo "NEXT_PUBLIC_API_MOCKING=disabled" >> .env

          # Sentry 설정
          echo "NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}" >> .env
          echo "NEXT_PUBLIC_SENTRY_AUTH_TOKEN=${{ secrets.NEXT_PUBLIC_SENTRY_AUTH_TOKEN }}" >> .env

      # Sentry 환경 변수 설정
      - name: Create Sentry environment file
        run: |
          echo "NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}" >> .env
          echo "SENTRY_ORG=jinii9" >> .env
          echo "SENTRY_PROJECT=wms" >> .env

      # EC2로 파일 복사
      - name: Copy files to EC2
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          source: ".,!node_modules,!.next,!.git"
          target: "/home/ubuntu/app"

      # EC2에서 Docker Compose 실행
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            cd /home/ubuntu/app
            docker-compose down --rmi all --volumes 
            docker system prune -af                 
            docker-compose up --build -d

github action secret

EC2_HOST : EC2 퍼블릭 IPv4 주소
EC2_USERNAME : ubuntu
EC2_SSH_KEY : EC2 접속 위해 받은 private key인 .pem 파일

review

github action 수행 파일workflow의 ci/cd 파일에서 gitignore되는 env 파일을 만들기 위해, env 환경변수를 하나씩 코드에 입력해주는 방법 말고, 한번에 파일을 복사해주는 방법으로 바꿔줘야겠다. 하나씩 ci/cd 파일에 환경변수를 적어주고, github secret에 등록해줘야 하는 동작을 여러번 해야하는게 너무 귀찮다.

배포 완료 후 에러 해결

NextAuth 에러

❗️ 로컬에서 NextAuth 사용할 때는 자동으로 NextAuth 실행에 필요한 토큰이 생성됐는데 배포하니까 해당 토큰들이 생기지 않아 CSRF 토큰 관련 에러를 발생시켰다.

[auth][error] MissingCSRF: CSRF token was missing during an action callback. Read more at https://errors.authjs.dev#missingcsrf

해결하기 위해 서버 로그를 확인해보자!
NextAuth가 호스트를 신뢰할 수 없다고 판단해서 발생하는 문제였다.

# 실시간 로그 확인
docker logs -f 컨테이너이름 

🙆🏻‍♀️ NextAuth 설정에 trustHost 옵션을 추가해준다.

import { IUser } from "@/custom";
import NextAuth, { Session } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { JWT } from "next-auth/jwt";

export const {
  handlers: { GET, POST },
  auth,
  signIn,
} = NextAuth({
  trustHost: true, // ✅ 추가
  secret: process.env.NEXTAUTH_SECRET,
  pages: {
    signIn: "/login",
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        const userData = user as IUser;
        token.userId = userData.userId;
        token.role = userData.role;
        token.phoneNumber = userData.phoneNumber;
        token.id = userData.id;
        token.name = userData.name;
      }
      return token;
    },

    async session({ session, token }: { session: Session; token: JWT }) {
      session.user = {
        userId: token.userId,
        name: token.name,
        role: token.role,
        phoneNumber: token.phoneNumber,
        id: token.id,
      };

      console.log("-----------SESSION CALLBACK:", session);
      return session;
    },
  },
  providers: [
    CredentialsProvider({
      async authorize(credentials): Promise<IUser> {
        // 로그인 페이지에서 백엔드 인증을 처리 완료 상태
        // credentials의 정보를 그대로 반환
        return credentials as unknown as IUser;
      },
    }),
  ],
});

api 연결 에러

❗️ 로컬에서는 잘만 연결되던 로그인 api가 배포되니까 에러가 발생하였다.

콘솔창에 api 요청 url의 환경변수 부분process.env.NEXT_PUBLIC_API_URL이 undefined로 뜨는 걸로 보아 프로덕션 환경에서 환경변수.env가 적용되지 않은 것 같아 실행중인 docker 컨테이너에서 환경변수를 확인해보았다.

# EC2 터미널
# 실행 중인 컨테이너에서 환경변수 확인
docker exec -it 컨테이너이름 env | grep NEXT

하지만 환경변수들은 잘 설정되어 있었다. -> Docker 환경 변수를 확인했을 때는 NEXT_PUBLIC_API_URL이 설정되어 있는데, 실제 애플리케이션에서 접근이 안되는 것으로 보인다.

❓ .env에는 환경변수가 많은데 NEXT_PUBLIC_API_URL 환경변수만 에러가 나는 이유
NEXT_PUBLIC_API_URL은 클라이언트 사이드에서 사용되는 환경변수아다.
하지만 다른 환경변수들은 서버 사이드에서만 사용되는 환경변수이다.

Next.js에서 NEXT_PUBLIC_ 접두사가 붙은 환경변수는 특별한 처리가 필요하다!
이 변수들은 빌드 시점에 애플리케이션에 포함되어야 하기. ㅐ문이다.

🙆🏻‍♀️ NEXT_PUBLIC_ 접두사가 붙은 환경변수는 특별한 처리를 해준다.

📁 docker-compose.yml
Docker 이미지 빌드 시점에 환경변수가 포함되도록 수정한다.
그리고 Dockerfile에서도 환경변수를 받을 수 있도록 수정한다.

name: Deploy to EC2

on:
  push:
    branches: [main]
  workflow_dispatch: # 수동 실행을 위한 트리거 추가

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      # Docker Compose 설치
      - name: Install Docker Compose
        run: |
          sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
          sudo chmod +x /usr/local/bin/docker-compose
          docker-compose version

      # 환경 변수 파일 생성
      - name: Create environment file
        run: |
          echo "NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}" >> .env
          echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" >> .env

          echo "NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}" >> .env
          echo "NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }}" >> .env

          # API Mocking 설정
          echo "NEXT_PUBLIC_API_MOCKING=disabled" >> .env
          # Sentry 설정
          echo "NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}" >> .env
          echo "NEXT_PUBLIC_SENTRY_AUTH_TOKEN=${{ secrets.NEXT_PUBLIC_SENTRY_AUTH_TOKEN }}" >> .env
          echo "SENTRY_ORG=jinii9" >> .env
          echo "SENTRY_PROJECT=wms" >> .env

      # Docker Hub 로그인
      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      # Docker 이미지 빌드 및 푸시
      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          build-args: | # ✅
            NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}
            NEXT_PUBLIC_BASE_URL=${{ secrets.NEXT_PUBLIC_BASE_URL }}
          tags: |
            ${{ secrets.DOCKERHUB_USERNAME }}/wms-frontend:latest
            ${{ secrets.DOCKERHUB_USERNAME }}/wms-frontend:${{ github.sha }}

      # EC2로 필요한 파일들만 복사
      - name: Copy deployment files to EC2
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          source: "docker-compose.yml,docker/nginx/default.conf,.env"
          target: "/home/ubuntu/app"

      # EC2에서 배포 실행
      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            echo "${{ secrets.DOCKERHUB_TOKEN }}" | sudo docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
            cd /home/ubuntu/app
            echo "DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}" >> .env
            sudo docker-compose pull
            sudo docker-compose down
            sudo docker-compose up -d

📁 Dokerfile
Dokerfile에서 환경변수가 빌드 시점에 주입되도록 수정해야 한다.

# 빌드
# nodejs의 버전을 명시해주고, builder라는 별칭을 부여
FROM node:22-alpine3.21 AS builder
# work 디렉토리를 /app으로 설정
WORKDIR /app

# ✅ 빌드 시점의 ARG 선언
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_BASE_URL

# ✅ 빌드 환경에서 사용할 ENV 설정
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL

# 패키지 파일 복사 및 설치
COPY package*.json package-lock.json ./
# npm 설치
RUN npm install

# 프로덕션 종속성만 설치
# RUN npm ci --only=production
COPY . .
# Next.js 빌드
# RUN npm run build
RUN npm run build --no-lint

# 실행
FROM node:22-alpine3.21 AS runner
WORKDIR /app
ENV NODE_ENV=production

# 런타임 환경변수 설정
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL

# 필요한 파일만 복사
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/server ./server
# Sentry source maps
COPY --from=builder /app/.next/server ./server
COPY --from=builder /app/.next/static ./.next/static 

# 오픈할 포트 설정
EXPOSE 3000
CMD ["node", "server.js"]
profile
Hi, I am HANI Developer(╹◡╹). .....1hani me?

0개의 댓글