Next.js + Docker + Cloud Run 자동 배포 파이프라인 구축
GitHub에 푸시하면 자동으로 Docker 이미지 빌드 → GCR 푸시 → Cloud Run 배포되는 CI/CD 파이프라인을 구축한 경험을 공유합니다.
목표
Git에 코드를 푸시하면 5분 이내에 프로덕션에 반영되는 자동화 파이프라인을 구축합니다.
git push origin main
↓
GitHub Actions 트리거
↓
Docker 이미지 빌드
↓
GCR(Container Registry) 푸시
↓
Cloud Run 배포
↓
서비스 URL 확인
1. Next.js standalone 모드 설정
Next.js는 standalone 출력 모드를 제공합니다. 이 모드를 사용하면 node_modules를 포함하지 않고도 실행 가능한 최소 번들을 생성합니다.
// next.config.ts
const config = {
output: "standalone", // 핵심 설정
};standalone의 장점:
- Docker 이미지 크기 대폭 감소 (수백 MB → 수십 MB)
- 빌드 캐시 효율 향상
- Cold start 시간 단축
2. Dockerfile 멀티스테이지 빌드
효율적인 Docker 이미지를 위해 멀티스테이지 빌드를 사용합니다.
# ========== Base ==========
FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app
# ========== Dependencies ==========
FROM base AS deps
COPY package.json package-lock.json* ./
COPY prisma ./prisma/
RUN npm ci
# ========== Builder ==========
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Prisma generate
RUN npx prisma generate
# Next.js 빌드
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# ========== Runner ==========
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
ENV HOSTNAME="0.0.0.0"
# 보안: non-root 사용자
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
COPY --from=builder /app/src/generated/prisma ./src/generated/prisma
USER nextjs
EXPOSE 8080
CMD ["node", "server.js"]각 스테이지 역할
| 스테이지 | 역할 | 결과 |
|---|---|---|
base | Node.js 베이스 이미지 | 공통 기반 |
deps | 의존성 설치 | node_modules 캐시 |
builder | 빌드 수행 | .next 산출물 |
runner | 실행 환경 | 최종 이미지 |
Prisma 주의사항
Prisma는 빌드 시점에 prisma generate로 클라이언트를 생성해야 합니다. 그리고 생성된 클라이언트를 runner 스테이지로 복사해야 런타임에 작동합니다.
# builder에서
RUN npx prisma generate
# runner에서
COPY --from=builder /app/src/generated/prisma ./src/generated/prisma3. GitHub Actions 워크플로우
.github/workflows/deploy.yml 파일을 생성합니다.
name: Deploy to Cloud Run
on:
push:
branches:
- main
- develop
env:
PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
SERVICE_NAME: develop-blog
REGION: asia-northeast3 # 서울 리전
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Google Auth
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
- name: Configure Docker
run: gcloud auth configure-docker --quiet
- name: Build Docker image
run: |
docker build -t gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} .
docker tag gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} \
gcr.io/$PROJECT_ID/$SERVICE_NAME:latest
- name: Push to Container Registry
run: |
docker push gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }}
docker push gcr.io/$PROJECT_ID/$SERVICE_NAME:latest
- name: Deploy to Cloud Run
run: |
gcloud run deploy $SERVICE_NAME \
--image gcr.io/$PROJECT_ID/$SERVICE_NAME:${{ github.sha }} \
--region $REGION \
--platform managed \
--allow-unauthenticated \
--memory 512Mi \
--cpu 1 \
--min-instances 0 \
--max-instances 3 \
--set-env-vars "DATABASE_URL=${{ secrets.DATABASE_URL }}" \
--set-env-vars "GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}"Cloud Run 설정 설명
| 옵션 | 값 | 설명 |
|---|---|---|
--memory | 512Mi | 인스턴스당 메모리 (무료 티어: 최대 2GB) |
--cpu | 1 | CPU 할당 |
--min-instances | 0 | 트래픽 없을 때 0으로 스케일 다운 (비용 절감) |
--max-instances | 3 | 최대 인스턴스 수 |
--allow-unauthenticated | - | 공개 접근 허용 |
4. GCP 서비스 계정 설정
GitHub Actions가 GCP에 접근하려면 서비스 계정이 필요합니다.
1) 서비스 계정 생성
# 서비스 계정 생성
gcloud iam service-accounts create github-actions \
--display-name="GitHub Actions"
# 필요한 역할 부여
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:github-actions@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/run.admin"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:github-actions@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.admin"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:github-actions@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser"2) 키 생성 및 GitHub Secrets 등록
# JSON 키 생성
gcloud iam service-accounts keys create key.json \
--iam-account=github-actions@$PROJECT_ID.iam.gserviceaccount.com생성된 key.json 내용을 GitHub 저장소의 Settings > Secrets > GCP_SA_KEY에 등록합니다.
5. 환경 변수 관리
프로덕션 환경의 민감한 정보는 GitHub Secrets에 저장하고, Cloud Run 배포 시 주입합니다.
--set-env-vars "DATABASE_URL=${{ secrets.DATABASE_URL }}" \
--set-env-vars "GEMINI_API_KEY=${{ secrets.GEMINI_API_KEY }}" \
--set-env-vars "GMAIL_USER=${{ secrets.GMAIL_USER }}" \
--set-env-vars "GMAIL_APP_PASSWORD=${{ secrets.GMAIL_APP_PASSWORD }}"Secret Manager를 사용하면 더 안전하게 관리할 수 있지만, 사이드 프로젝트 수준에서는 GitHub Secrets로 충분합니다.
6. Cloud SQL 연결
Cloud Run에서 Cloud SQL에 연결할 때는 --add-cloudsql-instances 옵션을 사용합니다.
--add-cloudsql-instances=$PROJECT_ID:$REGION:toy-dbDATABASE_URL은 Unix 소켓 형식으로 설정합니다:
postgresql://user:password@localhost/dbname?host=/cloudsql/project:region:instance
결과
현재 이 블로그는 위 파이프라인으로 운영 중입니다:
- 빌드 시간: 약 2분
- 배포 시간: 약 1분
- Cold start: 약 3초
- 월 비용: 거의 0원 (트래픽이 적으면 인스턴스 자동 종료)
배포 로그 예시
🚀 Deployed to:
https://develop-blog-xxxxxxxxxx-du.a.run.app
트러블슈팅
1. Prisma 클라이언트 not found
Error: @prisma/client did not initialize yet
→ runner 스테이지에서 src/generated/prisma 복사 확인
2. PORT 환경변수
Cloud Run은 PORT=8080을 사용합니다. Next.js standalone은 기본 3000 포트를 사용하므로 환경변수로 오버라이드해야 합니다.
ENV PORT=8080
ENV HOSTNAME="0.0.0.0"3. 이미지 크기 최적화
node:20-alpine 기반으로 이미지 크기를 최소화합니다. 최종 이미지는 약 150MB 정도입니다.
다음 단계
- Preview 환경 자동 생성 (PR 별 임시 배포)
- Canary 배포 전략
- 모니터링 대시보드 (Cloud Monitoring)
작은 사이드 프로젝트라도 자동화된 배포 파이프라인이 있으면 개발 속도가 확연히 달라집니다.