본문으로 건너뛰기
Previous
Next
GitHub Actions CI/CD Tutorial for Node.js | Test· Build

GitHub Actions CI/CD Tutorial for Node.js | Test· Build

GitHub Actions CI/CD Tutorial for Node.js | Test· Build

이 글의 핵심

GitHub Actions CI/CD tutorial for Node.js: lint and test on PRs, build Docker images, push to GHCR, deploy with SSH or your platform—cache, secrets, and reusable workflows.

Introduction

GitHub Actions reacts to repository events (push, PR, schedule) to automate test, build, and deploy. Node.js services often need install → lint → unit tests → build → Docker image → registry push → runtime deploy; wiring that in one pipeline cuts human error and release time. Production quality also depends on branch protection, secrets, cache strategy, and rollback. This article focuses on moving “works locally” into a reproducible workflow YAML.

What you will learn

  • How to chain test → build → image → deploy in one flow
  • actions/cache, multi-stage Docker builds, and per-environment jobs
  • Common failures (permissions, tags, registry auth) and debugging tips

Table of contents

  1. Concepts: CI/CD and GitHub Actions
  2. Hands-on: single workflow template
  3. Advanced: reusable workflows, matrix, cache
  4. Performance: cache and parallelism
  5. Real-world scenarios
  6. Troubleshooting
  7. Conclusion

Concepts: CI/CD and GitHub Actions

Terms

  • CI (Continuous Integration): On every merge, automatically install, build, and test to surface integration issues early.
  • CD (Continuous Delivery/Deployment): Promote validated artifacts to staging/production automatically or after approval.
  • Workflow: YAML describing event → job → step graphs.
  • Runner: GitHub-hosted (ubuntu-latest) or self-hosted machines executing jobs.

Why Node.js needs it

The ecosystem is sensitive to lockfiles, Node versions, and native addons. CI must use the same package-lock.json and Node version as production. Docker further pins OS and dependencies.

Hands-on: single workflow template

Example: test on PR, on push to main build/push image and deploy (SSH sketch). Save as .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

Behavior

  • test runs on PRs and pushes.
  • build-and-push runs only on main pushes.
  • deploy is illustrative—swap for kubectl, Fly.io, Railway, etc.

Minimal package.json scripts

{
  "scripts": {
    "lint": "eslint .",
    "test": "node --test",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Multi-stage 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"]

Advanced: reusable workflows, matrix, cache

Reusable workflow

# .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

Cross-platform matrix

strategy:
  fail-fast: false
  matrix:
    node: [20, 22]
    os: [ubuntu-latest, windows-latest]

npm cache

actions/setup-node with cache: npm is enough for most repos; monorepos may set cache-dependency-path.

Performance: cache and parallelism

TechniqueEffectNotes
npm ci + lockfileReproducible installsCI default
setup-node cacheFaster installsUse everywhere
Docker BuildKit cacheFaster image buildsRegistry or inline cache
Parallel jobsFaster PR feedbackWatch minute quotas on paid plans
Trade-off: Large matrices increase queue time and billing—often widen coverage on release branches only.

Real-world scenarios

  • Staging auto, production manual: environment: production with Required reviewers gates CD.
  • Tag releases: on: push: tags: ['v*'] to push v1.2.3 images.
  • Migrations: run npm run migrate in a deploy job with documented lock timeouts and rollback.

Troubleshooting

SymptomCauseFix
GHCR push deniedMissing token scopepermissions: packages: write on the job
npm ci failsLockfile driftRegenerate and commit lockfile locally
Failures only on one Node versionVersion mismatchPin engines and setup-node
Slow Docker buildsNo cacheBuildKit cache or --cache-from
Deploy runs old codePulling latest onlyDeploy by SHA or digest
Tip: Re-run failed jobs with debug logging; try act to reproduce workflows locally.

Conclusion

  • One pipeline for test → build → image → deploy removes repetitive work and mistakes.
  • npm ci, pinned Node, and multi-stage Docker are a solid baseline.
  • Next: align local and staging with Docker Compose for Node.js and read Node.js deployment for operations.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. GitHub Actions CI/CD tutorial for Node.js: lint and test on PRs, build Docker images, push to GHCR, deploy with SSH or y… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Node.js, GitHub Actions, CI/CD, Docker, Deployment, Automation 등으로 검색하시면 이 글이 도움이 됩니다.