GitHub Actions로 Node.js CI/CD 파이프라인 만들기 | 테스트·빌드·Docker·배포
이 글의 핵심
Node.js 앱에 대해 테스트·빌드·컨테이너 이미지·배포를 GitHub Actions 한 파이프라인으로 묶는 실전 워크플로입니다.
들어가며
GitHub Actions는 저장소 이벤트(push, PR, schedule 등)에 반응해 테스트·빌드·배포를 자동화하는 데 널리 쓰입니다. Node.js 서비스는 패키지 설치·린트·유닛 테스트·프런트 빌드·Docker 이미지 생성·레지스트리 푸시·런타임 배포까지 단계가 길어, 이를 한 파이프라인으로 묶어두면 휴먼 에러와 배포 시간을 동시에 줄일 수 있습니다.
프로덕션에서는 브랜치 보호, 시크릿 관리, 캐시 전략, 실패 시 롤백이 CI/CD 품질을 가릅니다. 이 글은 “로컬에서 되던 것”을 재현 가능한 워크플로 YAML로 옮기는 데 초점을 둡니다.
같은 GitHub Actions 패턴은 언어를 넘어 확장됩니다. 테스트 단계는 Node.js Jest·Python pytest·매트릭스 예제·Go go test와 비교하고, C++ 멀티 OS·캐시는 C++ GitHub Actions를, 배포는 Docker Compose·minikube·C++ Docker와 이어서 읽으면 그림이 맞습니다.
비유로 말씀드리면, CI/CD 파이프라인은 공장 라인에 가깝습니다—원료(clone) → 가공(install·lint·test·build) → 출고(image push) → 납품(deploy) 순서가 문서화되어 있어야 동일 품질이 반복됩니다.
이 글을 읽으면
- 테스트 → 빌드 → Docker 이미지 → 배포까지 한 흐름으로 설계하는 방법을 익히실 수 있습니다
actions/cache, 멀티 스테이지 빌드, 환경별 job 분리 패턴을 배우실 수 있습니다- 흔한 실패 원인(권한, 태그, 레지스트리 인증)과 디버깅 팁을 정리해 두었습니다
목차
개념: CI/CD와 GitHub Actions
기본 개념
- CI(Continuous Integration): 코드 병합 시마다 자동으로 설치·빌드·테스트해 통합 시점의 문제를 빨리 드러냅니다.
- CD(Continuous Delivery/Deployment): 검증된 산출물을 스테이징·프로덕션으로 자동(또는 승인 후) 배포합니다.
- Workflow: YAML로 정의된 이벤트 → 잡(job) → 스텝(step) 의 실행 그래프입니다.
- Runner: GitHub 호스티드 러너(ubuntu-latest 등) 또는 셀프호스티드 머신에서 잡이 돕니다.
왜 Node.js에 특히 필요한가
패키지 생태계는 lockfile, Node 버전, 네이티브 모듈에 민감합니다. 동일한 package-lock.json과 동일한 Node 버전으로 CI에서 돌리지 않으면 “내 PC에서는 됨”이 반복됩니다. Docker까지 포함하면 런타임 OS·의존성까지 고정할 수 있습니다.
실전: 단일 워크플로 템플릿
아래는 PR에서는 테스트만, main 병합 시 이미지 빌드·레지스트리 푸시·배포(SSH 예시)까지 이어지는 예시입니다. 프로젝트 루트에 .github/workflows/ci-cd.yml로 둡니다.
# .github/workflows/ci-cd.yml
name: CI/CD Node.js
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch: {}
env:
NODE_VERSION: '22'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
name: Lint & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint --if-present
- name: Unit tests
run: npm test --if-present
- name: Build (if applicable)
run: npm run build --if-present
build-and-push:
name: Docker Build & Push
needs: test
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy:
name: Deploy (example SSH)
needs: build-and-push
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy via SSH
uses: appleboy/[email protected]
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cd /opt/app && docker compose up -d --no-deps api
동작 요약
on.pull_request/on.push: 이벤트·브랜치에 맞춰 워크플로가 돕니다. PR과 main 푸시 모두에서 test 잡이 실행됩니다.workflow_dispatch: 수동 실행 버튼을 열어 배포·재시도에 씁니다.jobs.test→build-and-push→deploy순:needs: test로 테스트 통과 후에만 빌드가 이어집니다.build-and-push.if: main 브랜치에 push일 때만 이미지를 만들어 PR에선 레지스트리 푸시를 생략합니다.permissions.packages: write: 기본GITHUB_TOKEN으로 GHCR 푸시가 가능하도록 씁니다.docker/metadata-action의tags: 커밋 SHA 태그와 latest를 동시에 쓰면 추적·롤백이 쉬워집니다.- deploy 잡은 예시로 SSH 후
docker compose up -d --no-deps api로 API만 재기동합니다(-d는 백그라운드,--no-deps는 의존 서비스(Postgres 등)는 건드리지 않음). 실제로는 Kuberneteskubectl, PaaS CLI 등으로 바꿉니다.
실행 가능한 최소 앱 가정
package.json에 다음 스크립트가 있다고 가정합니다(없으면 해당 step을 제거하세요).
{
"scripts": {
"lint": "eslint .",
"test": "node --test",
"build": "tsc",
"start": "node dist/index.js"
}
}
Dockerfile 예시 (멀티 스테이지)
# Dockerfile
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
deps스테이지:npm ci --omit=dev로 프로덕션 의존성만 깔아 최종 이미지에 넣습니다.build스테이지: devDependencies 포함으로tsc등 빌드 도구를 쓰고, 산출물은dist에만 남깁니다.runner: 런타임에 필요한 파일·모듈만 복사해 이미지 크기와 공격 면을 줄입니다.
고급: 재사용 워크플로·매트릭·캐시
재사용 워크플로
여러 저장소에서 동일한 테스트 단계를 쓰려면 .github/workflows/reusable-test.yml에 workflow_call을 두고 호출합니다.
# .github/workflows/reusable-test.yml
on:
workflow_call:
inputs:
node-version:
required: true
type: string
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci && npm test
Node·OS 매트릭 (크로스 테스트)
strategy:
fail-fast: false
matrix:
node: [20, 22]
os: [ubuntu-latest, windows-latest]
npm 캐시
actions/setup-node의 cache: npm만으로도 대부분 충분합니다. 모노레포에서는 cache-dependency-path로 lockfile 경로를 지정합니다.
성능: 캐시와 병렬화
| 항목 | 효과 | 비고 |
|---|---|---|
npm ci + lockfile | 설치 재현성 | CI 표준 |
actions/setup-node cache | node_modules 복원 시간 단축 | 대부분의 Node 워크플로에 적용 |
| Docker BuildKit + 레이어 캐시 | 이미지 빌드 시간 단축 | cache-from/cache-to 또는 레지스트리 캐시 |
| 잡 병렬화 | PR 피드백 시간 단축 | lint/test를 분리할 때 유의 |
트레이드오프: 매트릭을 늘리면 분 단위 요금(유료 한도) 과 큐 대기가 늘 수 있습니다. 릴리스 브랜치에만 넓게 쓰는 편이 경제적일 때가 많습니다.
실무 사례
- 스테이징 자동 + 프로덕션 수동 승인:
environment: production에 Required reviewers를 두면 CD 단계가 승인 후에만 진행됩니다. - 태그 릴리스:
on: push: tags: ['v*']로 시맨틱 버전 태그에만 이미지v1.2.3을 박아 배포합니다. - 마이그레이션: 배포 직전 잡에서
npm run migrate를 실행하되, 락 타임아웃과 롤백 전략을 문서화합니다.
트러블슈팅
| 증상 | 원인 | 해결 |
|---|---|---|
permission denied GHCR 푸시 | GITHUB_TOKEN 권한 부족 | 잡에 permissions: packages: write |
npm ci 실패 | lockfile 불일치 | 로컬에서 npm install 후 lockfile 커밋 |
| 다른 Node에서만 실패 | 버전 차이 | engines 필드 + setup-node 버전 고정 |
| Docker 빌드만 느림 | 캐시 없음 | BuildKit 캐시 또는 --cache-from 이미지 |
| 배포는 됐는데 구 버전 | 태그 latest만 당김 | SHA 태그 배포 또는 이미지 다이제스트 고정 |
디버깅 팁: 실패한 잡에서 Re-run with debug logging을 켜고, 동일 컨테이너를 로컬에서 act(도구)로 재현할 수 있는지 확인합니다.
마무리
- 한 파이프라인으로 테스트·빌드·이미지·배포를 묶으시면 반복 작업과 실수를 줄일 수 있습니다.
- npm ci, Node 버전 고정, Docker 멀티 스테이지는 프로덕션 CI/CD의 기본 삼종 세트에 가깝습니다.
- 다음 단계로는 Docker Compose로 Node API·DB·Redis 한 번에 띄우기로 로컬·스테이징 환경을 맞추고, Node.js 배포 가이드와 함께 읽으면 운영 그림이 완성됩니다.
프로덕션 체크리스트
- 시크릿:
DEPLOY_HOST등은 저장소 Secrets에만 두고, 로그에 노출되지 않게 합니다. - 이미지 참조: 가능하면
latest만이 아니라 SHA 또는 다이제스트로 배포해 재현성을 높입니다. - 마이그레이션: 배포 직전 DB 마이그레이션 실패 시 롤백 절차를 팀에 공유합니다.