Deploy tự động từ GitHub lên VPS: webhook, Actions, không downtime
Ba cách deploy tự động từ GitHub lên VPS: GitHub Actions qua SSH, webhook, hoặc Git deploy của panel, kèm script release, health check và rollback.

Ý chính
- Có ba cách deploy tự động từ GitHub lên VPS - GitHub Actions SSH vào server, webhook gọi tới server, hoặc dùng panel có sẵn Git deploy.
- Webhook phải được xác thực bằng chữ ký HMAC-SHA256 trong header X-Hub-Signature-256, so sánh thời gian hằng.
- Deploy key của GitHub mặc định chỉ có quyền đọc và chỉ dùng cho một repository.
- Mỗi bản phát hành nên build trong thư mục riêng, qua health check rồi mới chuyển lưu lượng, để rollback chỉ là đổi lại con trỏ.
Có ba cách deploy tự động từ GitHub lên VPS mỗi khi bạn push code: GitHub Actions SSH vào server và chạy script deploy, webhook của GitHub gọi tới một endpoint trên server, hoặc dùng panel có sẵn Git deploy lo cả webhook, build, health check và rollback. Dù chọn cách nào, một quy trình đáng tin phải có bốn thứ: chỉ deploy đúng nhánh, xác thực người gọi, build bản mới ở chỗ riêng, và chỉ chuyển lưu lượng khi bản mới chạy được.
Bài này so sánh ba cách, đưa ra cấu hình GitHub Actions và script deploy dùng được ngay (có khoá, health check và rollback), giải thích cách xác thực webhook đúng chuẩn, và cách làm trên ZoPanel.
Deploy tự động từ GitHub: chọn cách nào?
| Cách | Ưu điểm | Nhược điểm |
|---|---|---|
| GitHub Actions + SSH | Chạy test trước khi deploy, log nằm trên GitHub, không phải mở endpoint mới trên server | Phải giữ khoá SSH trong GitHub Secrets; phải tự viết script release và rollback |
| Webhook tới server | Nhanh, không phụ thuộc runner của GitHub | Phải tự viết và bảo vệ endpoint, xử lý chữ ký, trùng lặp, chạy đồng thời |
| Panel có Git deploy | Webhook, build sandbox, health check, rollback có sẵn | Phụ thuộc vào panel; tuỳ biến trong khuôn khổ panel cho phép |
Một quy tắc chung: đừng bao giờ để quy trình deploy chạy dưới quyền root, và đừng để "deploy" chỉ là git pull vào thư mục đang chạy. Cách đó không có rollback, và bản lỗi lên production ngay lập tức.
Cách 1: GitHub Actions deploy qua SSH
Bước 1: Tạo user deploy và hai cặp khoá
Bạn cần hai cặp khoá khác nhau, hay bị nhầm:
- Khoá A (GitHub Actions → server): Actions dùng để SSH vào server. Private key nằm trong GitHub Secrets, public key nằm trong
authorized_keyscủa user deploy. - Khoá B (server → GitHub): server dùng để clone repository riêng tư. Public key thêm vào repository dưới dạng deploy key. Theo tài liệu GitHub, deploy key mặc định chỉ có quyền đọc và không dùng lại được cho repository khác.
sudo adduser --disabled-password --gecos "" deploy
sudo -u deploy ssh-keygen -t ed25519 -f /home/deploy/.ssh/id_ed25519 -N "" # khoá B
sudo cat /home/deploy/.ssh/id_ed25519.pub # thêm vào repo: Settings → Deploy keys
Bước 2: Khoá A chỉ được chạy đúng một lệnh
Trên máy của bạn, tạo khoá A (ssh-keygen -t ed25519 -f gha_deploy -N ""). Thêm public key vào /home/deploy/.ssh/authorized_keys kèm giới hạn:
restrict,command="/srv/myapp/deploy.sh" ssh-ed25519 AAAA...khoá-A... github-actions
restrict tắt chuyển tiếp cổng, agent và terminal; command= buộc mọi lần đăng nhập bằng khoá này chỉ chạy script deploy. Lộ khoá A thì kẻ tấn công chỉ chạy lại được bản deploy từ chính repository của bạn, không có shell.
Bước 3: Script deploy có release, health check và rollback
/srv/myapp/deploy.sh (chủ sở hữu deploy, quyền 750):
#!/usr/bin/env bash
set -euo pipefail
APP=/srv/myapp
REPO=git@github.com:you/myapp.git
REL="$APP/releases/$(date +%Y%m%d%H%M%S)"
exec 9>"$APP/deploy.lock"
flock -n 9 || { echo "Đang có deploy khác chạy" >&2; exit 1; }
PREV=""
[ -L "$APP/current" ] && PREV=$(readlink -f "$APP/current")
mkdir -p "$APP/releases"
git clone --depth 1 --branch main "$REPO" "$REL"
cd "$REL"
npm ci && npm run build # thay bằng lệnh build của dự án
ln -sfn "$REL" "$APP/current.new" && mv -Tf "$APP/current.new" "$APP/current"
sudo /usr/bin/systemctl restart myapp.service
if ! curl -fsS --retry 10 --retry-delay 2 --retry-connrefused \
http://127.0.0.1:3000/health > /dev/null; then
echo "Health check lỗi, quay về bản trước" >&2
if [ -n "$PREV" ]; then
ln -sfn "$PREV" "$APP/current.new" && mv -Tf "$APP/current.new" "$APP/current"
sudo /usr/bin/systemctl restart myapp.service
fi
exit 1
fi
ls -1dt "$APP"/releases/* | tail -n +6 | xargs -r rm -rf # giữ 5 bản
Dịch vụ systemd myapp.service chạy từ /srv/myapp/current. Cho phép user deploy khởi động lại đúng dịch vụ này, không hơn (tạo bằng sudo visudo -f /etc/sudoers.d/myapp-deploy):
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp.service
Đổi symlink bằng mv -T là thao tác nguyên tử: không có khoảnh khắc nào current trỏ vào thư mục dở dang. Tuy vậy, cách này vẫn có vài giây gián đoạn lúc dịch vụ khởi động lại. Muốn không gián đoạn thật sự, bạn cần chạy bản mới song song ở cổng khác rồi đổi upstream của nginx (blue/green), phần mà panel ở cách 3 làm sẵn.
Bước 4: Workflow GitHub Actions
Lưu known_hosts của server vào secret để Actions không tin nhầm server giả: chạy ssh-keyscan -t ed25519 203.0.113.10 từ một mạng bạn tin cậy, kiểm tra fingerprint khớp với server, rồi lưu vào secret DEPLOY_KNOWN_HOSTS. Private key A lưu vào DEPLOY_SSH_KEY.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy qua SSH
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
ssh deploy@203.0.113.10
Vì khoá A bị ép chạy deploy.sh, lệnh ssh trơn là đủ. concurrency đảm bảo hai lần push liên tiếp không deploy chồng lên nhau. Nếu dùng Fail2ban, nhớ rằng runner của GitHub có IP thay đổi liên tục; đăng nhập bằng khoá thành công không bị tính là thất bại.
Cách 2: Webhook GitHub gọi thẳng tới server
Webhook nhanh hơn Actions vì không chờ runner, nhưng endpoint của bạn công khai trên internet, nên phải làm đúng:
- Xác thực chữ ký. GitHub gửi header
X-Hub-Signature-256, là HMAC-SHA256 của body với secret bạn đặt. Theo tài liệu GitHub, phải so sánh bằng hàm thời gian hằng (ví dụhmac.compare_digesttrong Python), không dùng==. Header cũX-Hub-Signaturedùng SHA-1, chỉ còn để tương thích ngược. - Không đặt secret trong URL. URL nằm trong log của proxy và trình duyệt.
- Kiểm tra nhánh trong payload (
refbằngrefs/heads/main), bỏ qua mọi nhánh khác. - Trả lời ngay, deploy sau. Endpoint chỉ xếp việc vào hàng đợi rồi trả 200; build chạy nền. GitHub có thời hạn chờ phản hồi, và build dài sẽ bị coi là lỗi.
- Chống trùng lặp và chạy đồng thời. GitHub có thể gửi lại cùng một sự kiện; dùng header
X-GitHub-Deliveryđể bỏ qua bản trùng, và dùng khoá nhưflockở trên.
import hmac, hashlib
def valid_signature(secret: bytes, body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")
Endpoint chạy dưới user không có quyền root, và chỉ làm một việc: gọi script deploy ở cách 1.
Deploy tự động từ GitHub với ZoPanel
Nếu VPS chạy ZoPanel, Git deploy làm sẵn những phần khó ở trên:
- Tạo website ở Website → Thêm website, chọn Git deploy, nhập Địa chỉ repository và Nhánh. ZoPanel nhận diện framework (Next.js, Nuxt, Laravel, Django, Flask, Go, Rails...) và đề xuất lệnh cài đặt, build, chạy. Với repository riêng tư, bấm Hiện deploy key và thêm vào repository dưới dạng deploy key chỉ đọc.
- Ở tab Deploy, thẻ Tự động deploy, bật Deploy khi push lên nhánh main, sao chép Payload URL và Secret.
- Trên GitHub: Settings → Webhooks, dán URL, chọn content type
application/json, nhập secret.
ZoPanel kiểm tra chữ ký X-Hub-Signature-256 (hoặc header token với GitLab, Gitea), không bao giờ nhận secret qua URL, chỉ deploy khi push đúng nhánh, bỏ qua lần gửi trùng, khởi động tối đa một lần deploy qua webhook mỗi 15 giây và không bao giờ khi đang có deploy khác chạy.
Mỗi lần deploy, code được lấy vào thư mục release riêng, build dưới user của tài khoản hosting trong giới hạn CPU và RAM của gói. Với ứng dụng server, bản mới khởi động ở slot rảnh, phải trả lời Đường dẫn health check với mã dưới 500 trong 90 giây rồi nginx mới chuyển sang; bản lỗi thì bản cũ vẫn chạy nguyên và chủ website nhận thông báo Deploy bị lỗi. Các bản release giữ 5 bản gần nhất, Rollback cũng không gián đoạn.
Muốn chạy test trên GitHub Actions trước rồi mới deploy? Với license Pro, tạo API token ở Tài khoản của tôi → API token và gọi deploy từ bước cuối của workflow:
curl -fsS -X POST \
-H "Authorization: Bearer $ZOPANEL_TOKEN" \
https://panel.example.com:8888/api/sites/<site-id>/app/deploy
Lưu token trong GitHub Secrets và đặt thời hạn cho nó (30, 90 hoặc 365 ngày). Token Toàn quyền hành động như tài khoản của bạn, nên hãy coi nó như mật khẩu. Ví dụ cụ thể cho Python có trong bài deploy Python Django, Flask lên VPS, cho Node.js trong bài deploy Node.js lên VPS.
Những lỗi hay gặp khi tự động deploy
git pullvào thư mục đang chạy: không có rollback, và file nửa cũ nửa mới trong lúc pull.- Deploy bằng root: một dòng lệnh build bị chèn mã độc (ví dụ từ package npm) có toàn quyền server.
- Secret nằm trong repository: file
.envbị commit. Dùng biến môi trường trên server hoặc secret của CI. - Không có health check: bản không khởi động được vẫn được chuyển thành bản chạy chính.
- Hai deploy chạy chồng nhau: hai lần push liên tiếp ghi đè lẫn nhau. Luôn có khoá.
- Migration database không tương thích ngược: bản mới đổi schema, bản cũ đang chạy (hoặc bản rollback) gặp lỗi. Thêm cột trước, xoá cột ở lần phát hành sau.
Kết luận
Deploy tự động từ GitHub tiết kiệm thời gian mỗi ngày, nhưng chỉ đáng tin khi có xác thực, build tách biệt, health check và rollback. Bạn hoàn toàn tự dựng được bằng GitHub Actions, SSH với lệnh bị ép và một script release như trên. Nếu không muốn bảo trì phần đó cho từng dự án, một panel có sẵn Git deploy làm thay bạn.
Gói Free của ZoPanel có đầy đủ Git deploy, webhook và rollback cho 10 website; API token để deploy từ CI nằm trong gói Pro. Xem bảng giá và tài liệu ứng dụng Node.js và Python.
Câu hỏi thường gặp
Nên dùng GitHub Actions hay webhook để deploy tự động?
GitHub Actions hợp khi bạn muốn chạy test trước khi deploy và giữ log trên GitHub. Webhook nhanh hơn nhưng bạn phải tự bảo vệ endpoint. Nhiều đội kết hợp: Actions chạy test, bước cuối gọi deploy trên server.
Làm sao xác thực webhook của GitHub?
Đặt secret khi tạo webhook, rồi trên server tính HMAC-SHA256 của body với secret đó và so sánh với header X-Hub-Signature-256 bằng hàm so sánh thời gian hằng. Từ chối mọi request không khớp.
Deploy key của GitHub có an toàn không?
Deploy key mặc định chỉ có quyền đọc và gắn với đúng một repository, nên lộ khoá chỉ lộ code của repository đó. Đừng cấp quyền ghi nếu server không cần push.
Làm sao rollback khi bản deploy mới bị lỗi?
Build mỗi bản trong thư mục riêng và trỏ symlink current vào bản đang chạy. Rollback chỉ là trỏ symlink về bản trước rồi khởi động lại dịch vụ. Với ZoPanel, bấm Rollback ở thẻ Các bản release.
Có cần mở cổng mới trên VPS để nhận webhook không?
Thường là không: endpoint webhook chạy sau nginx trên cổng 443 như một website bình thường. Với ZoPanel, Payload URL nằm trên tên miền panel ở cổng 8888, vốn đã mở.
Đọc tiếp

Deploy ASP.NET Core lên VPS Linux
Cách deploy .NET lên Linux: cài .NET 10 trên Ubuntu 24.04, publish, chạy Kestrel bằng systemd sau nginx với SSL, làm tay hoặc bằng ZoPanel.

Deploy Laravel lên VPS: queue, scheduler và cập nhật không gián đoạn
Cách deploy Laravel lên VPS Ubuntu 24.04: PHP-FPM, nginx, .env, queue worker, scheduler và release không downtime, làm tay hoặc với ZoPanel.

Deploy Next.js lên VPS, không cần Vercel
Cách deploy Next.js lên VPS: output standalone, systemd, nginx cache /_next/static, SSL miễn phí, cập nhật không gián đoạn và những gì bạn mất so với Vercel.