[2026] GitHub Actions CI/CD Tutorial for Node.js | Test, Build, Docker & Deploy
이 글의 핵심
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.
다음은 yaml를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# .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/ssh-action@v1.0.3
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
아래 코드는 json를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
{
"scripts": {
"lint": "eslint .",
"test": "node --test",
"build": "tsc",
"start": "node dist/index.js"
}
}
Multi-stage Dockerfile
다음은 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
다음은 yaml를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# .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
아래 코드는 yaml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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.