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
- Concepts: CI/CD and GitHub Actions
- Hands-on: single workflow template
- Advanced: reusable workflows, matrix, cache
- Performance: cache and parallelism
- Real-world scenarios
- Troubleshooting
- 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
| Technique | Effect | Notes |
|---|---|---|
npm ci + lockfile | Reproducible installs | CI default |
setup-node cache | Faster installs | Use everywhere |
| Docker BuildKit cache | Faster image builds | Registry or inline cache |
| Parallel jobs | Faster PR feedback | Watch 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: productionwith Required reviewers gates CD. - Tag releases:
on: push: tags: ['v*']to pushv1.2.3images. - Migrations: run
npm run migratein a deploy job with documented lock timeouts and rollback.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| GHCR push denied | Missing token scope | permissions: packages: write on the job |
npm ci fails | Lockfile drift | Regenerate and commit lockfile locally |
| Failures only on one Node version | Version mismatch | Pin engines and setup-node |
| Slow Docker builds | No cache | BuildKit cache or --cache-from |
| Deploy runs old code | Pulling latest only | Deploy 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와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Docker Compose로 Node API·PostgreSQL·Redis 한 번에 띄우기
- Node.js 테스트 | Jest, Mocha, Supertest 완벽 가이드
- Node.js 배포 가이드 | PM2, Docker, AWS, Nginx
이 글에서 다루는 키워드 (관련 검색어)
Node.js, GitHub Actions, CI/CD, Docker, Deployment, Automation 등으로 검색하시면 이 글이 도움이 됩니다.