Tự động hóa CI/CD cho Node.js với GitHub Actions: Hướng dẫn từ Zero đến Deploy VPS
Chạy lệnh git push xong, code tự động chạy test, tự động build Docker image và tự động cập nhật trên VPS — đó không phải là viễn cảnh xa xôi, mà là điều bạn có thể làm được ngay sau khi đọc xong hướng dẫn này. Là lập trình viên Node.js, việc ngồi chờ npm run build và gõ SSH mỗi lần sửa bug là quá khứ. Đã đến lúc để máy làm việc, còn bạn tập trung vào tính năng mới.
Bài viết này sẽ hướng dẫn bạn xây dựng một pipeline CI/CD thực chiến, đưa code từ máy local lên VPS một cách tự động, an toàn và chuyên nghiệp.
1. Yêu cầu tiên quyết
Trước khi bắt đầu, hãy đảm bảo bạn đã chuẩn bị:
- Một dự án Node.js (có
package.json, scripttest, và tùy chọn scriptlint). - Tài khoản GitHub và repository (public hoặc private).
- Tài khoản Docker Hub (để lưu trữ image).
- Một VPS (Ubuntu 20.04/22.04) có cài Docker Engine và Docker Compose (phiên bản V2 hoặc V1).
Gợi ý: DigitalOcean, Vultr, Linode, hoặc các nhà cung cấp trong nước với giá từ 5-6 USD/tháng. - Kiến thức cơ bản về Git, Node.js, Docker.
2. CI/CD là gì và tại sao Developer cần nó?
CI/CD là viết tắt của Continuous Integration (Tích hợp liên tục) và Continuous Deployment (Triển khai liên tục). Nó tự động hóa việc xây dựng, kiểm thử và triển khai phần mềm mỗi khi bạn push code.
Continuous Integration (CI) – khi push code lên repository, hệ thống sẽ tự động:
- Lấy code mới nhất.
- Cài dependencies.
- Chạy linter và unit test.
Continuous Deployment (CD) – sau khi CI thành công, hệ thống tự động deploy ứng dụng lên server mà không cần can thiệp thủ công.
Lợi ích chính:
- ⏱ Tiết kiệm thời gian – không cần deploy thủ công mất 15 phút mỗi lần.
- 🛡 Giảm sai sót – tránh quên cài dependency hay chạy test trước khi merge.
- 🔍 Phát hiện lỗi sớm – test báo đỏ ngay trên Pull Request, giúp sửa lỗi trước khi ảnh hưởng người dùng.
Dưới góc nhìn kỹ thuật, CI/CD biến quy trình deploy thành script có thể lặp lại, quản lý phiên bản, và kiểm soát được.
3. Cấu trúc file GitHub Actions Workflow
Workflow là file YAML đặt trong thư mục .github/workflows/ của repository.
Cấu trúc cơ bản:
name: Tên workflow
on: [push, pull_request] # sự kiện kích hoạt
jobs:
ten_job:
runs-on: ubuntu-latest
steps:
- name: Bước 1
uses: some/action@version
- name: Bước 2
run: echo "Hello"
Bảng giải thích các thành phần chính:
| Thành phần | Ý nghĩa | Bắt buộc |
|---|---|---|
name |
Tên hiển thị trên GitHub UI | Không |
on |
Sự kiện kích hoạt (push, pull_request, schedule) | Có |
jobs |
Tập hợp các job | Có |
runs-on |
Môi trường runner (Ubuntu, Windows, macOS) | Có |
steps |
Các bước trong job | Có |
uses |
Dùng action có sẵn | Không |
run |
Chạy lệnh shell trực tiếp | Không |
Khác với Jenkins, GitHub Actions không yêu cầu bạn tự quản lý runner server. GitHub cung cấp sẵn máy ảo (Ubuntu, Windows, macOS) có cài sẵn Node.js, Docker, và nhiều công cụ khác.
4. Thực hành: Pipeline 3 giai đoạn
Chúng ta sẽ xây dựng pipeline với 3 giai đoạn tuần tự:
- Lint + Unit Test (CI)
- Build Docker image & Push lên Docker Hub
- SSH vào VPS, pull image và chạy container
Giai đoạn 1: Lint & Test
Tạo file .github/workflows/deploy.yml:
name: CI/CD Deploy to VPS
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
# ===== JOB 1: KIỂM TRA CHẤT LƯỢNG CODE =====
test:
name: Lint & Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false # Bảo mật: không để lại GITHUB_TOKEN
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
- name: Run Linter (if script exists)
run: |
if npm run | grep -q '"lint"'; then
npm run lint
else
echo "No lint script found, skipping"
fi
- name: Run Tests
run: npm test
Lưu ý: Script test bắt buộc phải có trong package.json (ví dụ "test": "jest"). Script lint là không bắt buộc, nhưng nếu có, nó sẽ chạy và bắt lỗi coding convention.
Giai đoạn 2: Build Docker & Push lên Docker Hub
Thêm job thứ hai chỉ chạy khi test thành công và ở nhánh main:
# ===== JOB 2: BUILD & PUSH DOCKER IMAGE =====
build-and-push:
name: Build Docker Image
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/my-node-app:latest
${{ secrets.DOCKERHUB_USERNAME }}/my-node-app:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Yêu cầu thêm permission (nếu dùng cache type=gha mode=max) – bạn cần thêm vào đầu file workflow:
permissions:
contents: read
packages: write
Dockerfile mẫu (multi-stage build, bảo mật)
Tạo file Dockerfile tại thư mục gốc dự án:
# Stage 1: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Giai đoạn 3: SSH vào VPS và Deploy
Job cuối – kết nối VPS, pull image mới và restart container:
# ===== JOB 3: DEPLOY LÊN VPS =====
deploy:
name: Deploy to VPS
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/main'
steps:
- name: Set up SSH key
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.VPS_SSH_PRIVATE_KEY }}
- name: Add VPS to known hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.VPS_IP }} >> ~/.ssh/known_hosts
- name: Deploy with Docker Compose
run: |
ssh ${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }} << 'EOF'
cd /app/my-node-app
docker compose pull
docker compose up -d --remove-orphans
docker system prune -f
EOF
Ghi chú: Lệnh docker compose (không dấu gạch ngang) dành cho Compose V2. Nếu VPS của bạn dùng Compose V1, hãy thay bằng docker-compose. Để kiểm tra: docker compose version.
File docker-compose.yml trên VPS
Tạo thư mục /app/my-node-app trên VPS và đặt file docker-compose.yml:
version: '3.8'
services:
app:
image: your-dockerhub-username/my-node-app:latest
container_name: nodejs-app
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
interval: 30s
timeout: 5s
retries: 3
5. Quản lý Secrets an toàn
Tuyệt đối không hardcode SSH key, database password hay token trong code.
GitHub Secrets là các biến được mã hóa. Truy cập: Repository → Settings → Secrets and variables → Actions → New repository secret.
| Secret name | Giá trị | Mô tả |
|---|---|---|
DOCKERHUB_USERNAME |
Tên user Docker Hub | Đăng nhập Docker Hub |
DOCKERHUB_TOKEN |
Personal Access Token (tạo tại Docker Hub → Security) | Xác thực an toàn, không dùng mật khẩu |
VPS_IP |
Địa chỉ IP của VPS | SSH đến VPS |
VPS_USER |
Tên user SSH (ví dụ root, ubuntu) |
Login vào VPS |
VPS_SSH_PRIVATE_KEY |
Nội dung file private key | Xác thực SSH |
Cách tạo SSH key (không passphrase)
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github-actions-deploy -N ""
cat ~/.ssh/github-actions-deploy.pub # copy lên VPS vào ~/.ssh/authorized_keys
cat ~/.ssh/github-actions-deploy # copy toàn bộ vào secret VPS_SSH_PRIVATE_KEY
Trên VPS:
mkdir -p ~/.ssh
echo "ssh-ed25519 AAA... (paste public key)" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
6. Lỗi thường gặp và cách khắc phục
🔴 Lỗi 1: SSH permission denied (publickey)
Nguyên nhân: Thiếu public key trên VPS, hoặc private key sai định dạng.
Cách khắc phục:
- Kiểm tra lại public key trong
~/.ssh/authorized_keys. - Đảm bảo private key không có passphrase.
- Xác nhận
VPS_SSH_PRIVATE_KEYchứa cả header-----BEGIN OPENSSH PRIVATE KEY-----.
🔴 Lỗi 2: docker: command not found hoặc docker compose: command not found
Nguyên nhân: VPS chưa cài Docker hoặc Docker Compose.
Cách khắc phục: Chạy trên VPS một lần:
# Cài Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Cài Docker Compose V2 (plugin)
mkdir -p ~/.docker/cli-plugins/
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose
🔴 Lỗi 3: Cache Docker không hoạt động
Nguyên nhân: Thiếu permission packages: write hoặc dùng mode=max không được hỗ trợ.
Cách khắc phục: Thêm vào đầu workflow:
permissions:
contents: read
packages: write
7. Best Practices cho pipeline chuyên nghiệp
Áp dụng những nguyên tắc sau để pipeline chạy nhanh, an toàn và dễ bảo trì.
✅ Cache node_modules và Docker layers
Cache giúp giảm thời gian chạy từ 1-2 phút xuống 10-20 giây.
✅ Phân chia job rõ ràng
Mỗi job chỉ làm một việc (test, build, deploy). Dùng needs để định nghĩa thứ tự.
✅ Chỉ deploy từ nhánh main
if: github.ref == 'refs/heads/main'
✅ Tắt persist-credentials trong actions/checkout
- uses: actions/checkout@v4
with:
persist-credentials: false
Điều này ngăn chặn token có quyền ghi bị lộ trong supply‑chain attacks.
✅ Pin action version bằng commit SHA (khuyến nghị bảo mật cao)
Thay vì dùng @v4, bạn có thể dùng SHA cụ thể:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
Tìm SHA tại actions/checkout.
8. FAQ
❓ GitHub Actions có miễn phí không?
- Public repositories: Hoàn toàn miễn phí, không giới hạn.
- Private repositories: 2.000 phút/tháng miễn phí cho GitHub-hosted runners (Ubuntu, Windows, macOS). Dùng thêm có thể tính phí theo mức $0.008/phút (Ubuntu).
- Self-hosted runners: Miễn phí không giới hạn phút (bạn chỉ trả chi phí cho server tự host).
❓ Tôi có thể deploy lên Shared Hosting không?
Không. Pipeline này yêu cầu VPS có SSH và Docker. Shared hosting thường không cho phép cài Docker hoặc chạy container. Hãy dùng VPS cấu hình thấp (1GB RAM, 1 CPU) với giá ~5-6 USD/tháng.
❓ Làm sao để deploy lên nhiều VPS cùng lúc?
Bạn có thể mở rộng job deploy bằng matrix strategy hoặc dùng action như appleboy/ssh-action với danh sách server.
9. Kết luận
Bạn đã có một pipeline CI/CD hoàn chỉnh cho ứng dụng Node.js: từ git push đến khi ứng dụng chạy trên VPS, mọi thứ đều tự động. Quy trình này không chỉ tiết kiệm thời gian mà còn nâng tầm dự án của bạn lên một đẳng cấp chuyên nghiệp.
Pipeline có thể mở rộng thêm:
- Gửi thông báo Slack/Telegram khi deploy thành công.
- Tích hợp SonarQube để phân tích chất lượng code.
- Deploy lên nhiều VPS hoặc Kubernetes.
DevOps không phải thứ gì đó xa vời. Hãy bắt đầu từ workflow nhỏ này, bạn sẽ dần làm chủ được toàn bộ vòng đời phần mềm – một kỹ năng quan trọng để tiến lên Senior và Technical Lead.
✅ Checklist hoàn thành sau khi đọc bài:
- Tạo repository GitHub và cấu hình secrets.
- Thêm file
.github/workflows/deploy.ymlvới nội dung trên. - Chuẩn bị Dockerfile và docker-compose.yml.
- Thiết lập VPS (cài Docker, Docker Compose, public key).
- Chạy thử push lên nhánh
mainvà kiểm tra workflow trên GitHub Actions.
Tài liệu tham khảo chính thức: