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.
Mục lục bài viết
VPS & Hosting cho dự án của bạn
Khám phá các gói máy chủ và lưu trữ phù hợp từ website cá nhân đến ứng dụng production.
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:
- Thêm cấu trúc mới.
- Deploy code hỗ trợ cả cũ và mới.
- Chuyển dữ liệu.
- Ngừng dùng cấu trúc cũ.
- 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
Bài viết liên quan
Deploy Node.js production bằng PM2 và Nginx trên VPS
Hướng dẫn deploy ứng dụng Node.js bằng PM2, ecosystem file, biến môi trường, startup, log, Nginx reverse proxy và rollback.
Đọc bài →Domain và DNS là gì? Cách trỏ tên miền về VPS đúng cách
Giải thích domain, DNS, nameserver, bản ghi A, AAAA, CNAME, TXT và quy trình trỏ tên miền về VPS để deploy website.
Đọc bài →SSH Key là gì? Đăng nhập VPS an toàn không cần mật khẩu
Hướng dẫn tạo SSH key, thêm public key vào VPS, cấu hình SSH alias, sửa permission và xử lý lỗi publickey trên Windows, Linux.
Đọc bài →