DeployEasy
CI/CDTrung bình

GitHub Actions CI/CD: Tự động deploy Node.js lên VPS

Tạo GitHub Actions CI/CD để build, test và tự động deploy Node.js lên VPS qua SSH an toàn, có secrets, healthcheck, concurrency và rollback.

· 8 phút đọc· 1.610 từ
Mục lục bài viết

Pipeline GitHub Actions CI CD tự động deploy Node.js lên VPS

GitHub Actions deploy VPS giúp thay quy trình SSH thủ công bằng pipeline có thể lặp lại: checkout code, cài dependency, lint, test, build, deploy và healthcheck. Nhưng workflow chỉ “xanh” chưa đảm bảo ứng dụng production hoạt động nếu bạn không kiểm tra host key, quyền user deploy, concurrency, migration và rollback.

Bài viết xây dựng CI trước rồi mới thêm CD qua SSH. Private key được lưu trong GitHub Secrets, server dùng deploy script cố định và pipeline chỉ thành công khi endpoint health trả về kết quả hợp lệ.

Tóm tắt nhanh: Tách CI và deploy; dùng key riêng cho automation; xác minh host key; giới hạn quyền user deploy; khóa concurrency; healthcheck sau deploy và chuẩn bị rollback trước khi bật tự động hóa production.

Sơ đồ luồng cho GitHub Actions deploy VPS

Demo pipeline: deploy chỉ thành công sau healthcheck

Một workflow nên thất bại khi website không phản hồi, thay vì kết thúc ngay sau lệnh restart. Đoạn kiểm tra dưới đây cho phép curl --fail trả exit code khác 0 và dừng job.

Terminal demo cho GitHub Actions deploy VPS

npm ci
npm run lint
npm test
npm run build
ssh deploy@server sudo /usr/local/bin/deploy-app
curl --fail --retry 5 --retry-delay 3 https://api.example.com/health

Lưu ý: Output trong ảnh và ví dụ là demo lab đã ẩn thông tin nhạy cảm. Khi chụp terminal thật, hãy che IP quản trị, username nội bộ, token, private key, mật khẩu và chuỗi kết nối database.

CI và CD là gì?

Continuous Integration

Mỗi thay đổi được tự động:

  • Cài dependency.
  • Lint.
  • Type check.
  • Test.
  • Build.

Continuous Delivery/Deployment

Sau khi kiểm tra thành công, artifact hoặc image được đưa tới staging/production.

Push/Pull Request

Install

Lint + Test

Build

Deploy

Healthcheck

Bắt đầu bằng CI trước

Đừng tự động deploy ngay từ workflow đầu tiên. Hãy làm lint, test và build ổn định trên pull request trước.

Workflow CI

Tạo:

.github/workflows/ci.yml
name: CI

on:
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test -- --runInBand

      - name: Build
        run: npm run build

Nếu project chưa có script, hãy thêm script đúng thay vì dùng || true để che lỗi.

Action bên thứ ba

Chỉ dùng action từ nguồn đáng tin cậy. Đọc quyền action yêu cầu. Với hệ thống quan trọng, cân nhắc pin theo commit SHA và dùng công cụ cập nhật dependency.

Mô hình deploy qua SSH

GitHub runner
      ↓ SSH
VPS chạy deploy script

Tạo key riêng cho deployment, không dùng private key cá nhân.

Tạo deployment key

ssh-keygen \
  -t ed25519 \
  -C "github-actions-deploy" \
  -f github-actions-deploy

Tạo:

github-actions-deploy
github-actions-deploy.pub
  • Public key thêm vào VPS.
  • Private key lưu GitHub Secret.
  • User deploy có quyền tối thiểu.

Thêm public key lên VPS

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

GitHub Secrets

Trong repository:

Settings
→ Secrets and variables
→ Actions

Tạo:

VPS_HOST
VPS_PORT
VPS_USER
VPS_SSH_KEY
VPS_HOST_KEY

Không in secret ra log.

Xác minh host key

Không chấp nhận host key mù quáng.

Trên VPS:

sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

Hoặc lấy public host key qua console tin cậy:

cat /etc/ssh/ssh_host_ed25519_key.pub

Lưu known_hosts đã xác minh vào secret.

Deploy script trên VPS

Tạo:

/usr/local/bin/deploy-deployeasy
#!/usr/bin/env bash

set -Eeuo pipefail

APP_DIR="/var/www/deployeasy-api"
BRANCH="main"

cd "$APP_DIR"

git fetch origin "$BRANCH"
git checkout "$BRANCH"
git reset --hard "origin/$BRANCH"

npm ci
npm run build

pm2 restart ecosystem.config.cjs --update-env

curl --fail --silent --show-error \
  http://127.0.0.1:3001/health

Permission:

sudo chmod 750 /usr/local/bin/deploy-deployeasy

Production directory không nên chứa sửa đổi thủ công vì git reset --hard sẽ xóa chúng.

Workflow deploy

.github/workflows/deploy.yml
name: Deploy production

on:
  push:
    branches:
      - main

permissions:
  contents: read

concurrency:
  group: production
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest

    environment:
      name: production
      url: https://api.deployeasy.vn

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - name: Install
        run: npm ci

      - name: Test
        run: npm test -- --runInBand

      - name: Build
        run: npm run build

      - name: Prepare SSH
        env:
          SSH_PRIVATE_KEY: ${{ secrets.VPS_SSH_KEY }}
          VPS_HOST_KEY: ${{ secrets.VPS_HOST_KEY }}
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          printf '%s\n' "$VPS_HOST_KEY" > ~/.ssh/known_hosts
          chmod 600 ~/.ssh/known_hosts

      - name: Deploy
        env:
          VPS_HOST: ${{ secrets.VPS_HOST }}
          VPS_PORT: ${{ secrets.VPS_PORT }}
          VPS_USER: ${{ secrets.VPS_USER }}
        run: |
          ssh \
            -i ~/.ssh/deploy_key \
            -p "$VPS_PORT" \
            "$VPS_USER@$VPS_HOST" \
            "/usr/local/bin/deploy-deployeasy"

      - name: Verify
        run: |
          curl \
            --fail \
            --retry 5 \
            --retry-delay 5 \
            https://api.deployeasy.vn/health

GitHub Environment

Environment production có thể dùng:

  • Environment secrets.
  • Required reviewers.
  • Deployment branches.
  • Protection rules.
  • Lịch sử deployment.

Khả năng áp dụng phụ thuộc loại repository và gói GitHub hiện tại.

Concurrency

concurrency:
  group: production
  cancel-in-progress: false

Ngăn hai deployment chạy chồng nhau.

Hủy deploy đang chạy có thể để server ở trạng thái dở dang, nên không bật cancel-in-progress: true thiếu đánh giá.

Deploy artifact thay vì build trên VPS

Quy trình:

Runner build

Artifact
    ↓ SCP
VPS giải nén

PM2 restart

Đóng gói:

- name: Create artifact
  run: |
    tar -czf release.tar.gz \
      dist \
      package.json \
      package-lock.json \
      ecosystem.config.cjs

Upload:

- name: Upload artifact
  run: |
    scp \
      -i ~/.ssh/deploy_key \
      -P "$VPS_PORT" \
      release.tar.gz \
      "$VPS_USER@$VPS_HOST:/tmp/release.tar.gz"

Ưu điểm:

  • VPS không cần nhiều RAM build.
  • Artifact đã được CI kiểm tra.
  • Dễ version và rollback.

Deploy Docker image

CI build image

Push GHCR

VPS pull image

Migration

docker compose up -d

Tag bằng commit SHA, không chỉ latest.

Ví dụ:

env:
  IMAGE: ghcr.io/${{ github.repository }}/api

- name: Build
  run: |
    docker build \
      -t "$IMAGE:${{ github.sha }}" \
      .

GHCR permissions

permissions:
  contents: read
  packages: write

Login:

- name: Login to GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

Kiểm tra phiên bản action và tài liệu hiện hành trước production.

Migration

Thứ tự an toàn hơn:

Backup
Deploy code tương thích
Run migration
Switch/restart
Healthcheck

Migration phá backward compatibility có thể làm rollback code thất bại.

Chiến lược expand/contract:

  1. Thêm cấu trúc mới.
  2. Deploy code hỗ trợ cả cũ và mới.
  3. Chuyển dữ liệu.
  4. Ngừng dùng cấu trúc cũ.
  5. Xóa ở release sau.

Rollback

Artifact:

releases/
├── git-a1b2c3d
├── git-d4e5f6g
└── current -> git-d4e5f6g

Docker:

API_IMAGE_TAG=git-a1b2c3d
docker compose pull
docker compose up -d

Không rollback database mù quáng.

Healthcheck sau deploy

Nội bộ:

curl -f http://127.0.0.1:3001/health

Public:

curl -f https://api.deployeasy.vn/health

Có thể kiểm tra version endpoint và một request read-only.

Least privilege

Deploy user không nên có sudo ALL.

Chỉ cấp quyền cần:

  • App directory.
  • PM2 của chính user.
  • Script giới hạn.
  • Docker nếu cần, nhưng Docker group gần tương đương root.

Bảo vệ secrets

  • Không echo.
  • Tách staging/production.
  • Rotate deploy key.
  • Xóa key không còn dùng.
  • Không cho workflow từ fork truy cập secret.
  • Review action bên thứ ba.

Lỗi thường gặp

Permission denied (publickey)

  • Sai user.
  • Sai private key.
  • Public key chưa thêm.
  • Permission sai.
  • Secret mất newline.

Host key verification failed

Known_hosts thiếu hoặc host key thay đổi. Xác minh fingerprint trước khi cập nhật.

Build local pass, CI fail

  • Node version khác.
  • Tên file khác chữ hoa/thường.
  • Missing env.
  • Lock file không đồng bộ.
  • Test phụ thuộc local.

Workflow xanh nhưng app lỗi

Cần public healthcheck và log server.

Checklist

[ ] PR chạy lint/test/build
[ ] Deploy chỉ từ branch được phép
[ ] Environment production
[ ] Concurrency
[ ] Deploy key riêng
[ ] Host key đã xác minh
[ ] Secret không trong YAML
[ ] Deploy user giới hạn quyền
[ ] Artifact/image có version
[ ] Backup trước migration
[ ] Healthcheck nội bộ/public
[ ] Rollback đã thử
[ ] Log không lộ secret

Câu hỏi thường gặp

Có nên deploy mỗi push vào main?

Chỉ khi branch protection, test và rollback đủ tin cậy. Production quan trọng có thể cần approval.

Có cần SSH vào VPS không?

Không bắt buộc. Có thể deploy qua registry hoặc API cloud.

Có nên chạy self-hosted runner trên VPS production?

Cần đánh giá bảo mật kỹ vì workflow có thể thực thi code trên server.

Bài viết liên quan

Kết luận

Pipeline tốt không chỉ tự động chạy lệnh. Nó phải có quyền tối thiểu, environment được bảo vệ, concurrency, healthcheck và rollback.

Nguồn tham khảo

Đọc tiếp