DeployEasy
Menu
CI/CDTrung bình

CI/CD với GitHub Actions: Tự động deploy lên VPS

Hướng dẫn tạo GitHub Actions workflow build, test và deploy lên VPS bằng SSH, quản lý secrets, environment, concurrency và rollback.

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

CI/CD giúp tự động hóa kiểm tra và phát hành code. Thay vì SSH vào VPS, pull code rồi chạy lệnh thủ công, GitHub Actions có thể thực hiện pipeline khi code được merge vào main.

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.

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