2 giờ sáng. Khách hàng nhắn tin báo lỗi. Mình debug xong, push fix lên GitHub, rồi lại mở terminal: ssh root@server, rồi cd /var/www/app && git pull && npm install && pm2 restart app. Xong lúc 3 giờ, đi ngủ. Hôm sau lại làm y chang. Cái vòng lặp này kéo dài mấy tháng cho đến khi mình chịu khó ngồi setup webhook một lần.
Dự án web app gần nhất có 5 developer — sau khi áp dụng quy trình này, mọi thứ đổi khác hẳn. Từ lúc push code đến khi production cập nhật chỉ còn dưới 60 giây. Không cần ai ngồi canh. Không ai quên deploy vì đang buồn ngủ.
Webhook hoạt động như thế nào
Đơn giản thôi: webhook là một HTTP endpoint trên server của bạn. Khi bạn push code lên GitHub, GitHub gửi một POST request đến endpoint đó kèm thông tin về commit. Server nhận được, verify xem có đúng từ GitHub không, rồi chạy deploy script.
GitHub Actions đòi bạn viết YAML, cấu hình runner, quản lý secrets/environments. Jenkins còn nặng hơn. Webhook thì không — đủ đơn giản để tự dựng trong một buổi sáng, đủ ổn định để chạy production nhiều tháng không cần đụng lại.
Developer push code lên GitHub
↓
GitHub gửi POST → https://yourserver.com/webhook
↓
Webhook server verify secret key
↓
Chạy: git pull → install deps → restart service
↓
Production cập nhật ✓
Thực hành: Dựng webhook từ đầu
Bước 1: Viết webhook server bằng Python
Mình chọn Python + Flask — gọn, dễ debug lúc đêm khuya, và không cần học gì thêm nếu bạn đã biết Python. Cài Flask trên server trước:
pip3 install flask
Tạo file /var/www/webhook/webhook.py:
import hmac
import hashlib
import subprocess
import os
from flask import Flask, request, abort
app = Flask(__name__)
# Lấy từ biến môi trường, KHÔNG hardcode vào code
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "").encode()
DEPLOY_SCRIPT = "/var/www/webhook/deploy.sh"
def verify_signature(payload_body, signature_header):
"""Xác thực request đến từ GitHub, không phải ai đó random."""
if not signature_header:
return False
hash_object = hmac.new(WEBHOOK_SECRET, msg=payload_body, digestmod=hashlib.sha256)
expected = "sha256=" + hash_object.hexdigest()
return hmac.compare_digest(expected, signature_header)
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-Hub-Signature-256")
if not verify_signature(request.data, signature):
abort(403)
payload = request.json
# Chỉ deploy khi push lên nhánh main
if payload.get("ref") != "refs/heads/main":
return {"status": "skipped", "reason": "not main branch"}, 200
result = subprocess.run(
[DEPLOY_SCRIPT],
capture_output=True,
text=True,
timeout=120
)
if result.returncode != 0:
print(f"Deploy failed:\n{result.stderr}")
return {"status": "error", "message": result.stderr}, 500
return {"status": "success"}, 200
if __name__ == "__main__":
app.run(host="127.0.0.1", port=9000)
Bước 2: Viết deploy script
Tạo file /var/www/webhook/deploy.sh. Script này là nơi bạn viết logic deploy thực tế — git pull, cài deps, restart service:
#!/bin/bash
set -e # Dừng ngay nếu bất kỳ lệnh nào lỗi
APP_DIR="/var/www/myapp"
LOG_FILE="/var/log/deploy.log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting deploy..." >> $LOG_FILE
cd $APP_DIR
git pull origin main >> $LOG_FILE 2>&1
npm install --production >> $LOG_FILE 2>&1
pm2 restart myapp >> $LOG_FILE 2>&1
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy completed ✓" >> $LOG_FILE
chmod +x /var/www/webhook/deploy.sh
Bước 3: Chạy webhook server với systemd
Đừng chạy Flask bằng python webhook.py & rồi để đó — process sẽ chết khi server reboot, và bạn sẽ mất một lúc mới nhận ra tại sao deploy không chạy nữa. Dùng systemd để nó tự restart:
sudo nano /etc/systemd/system/webhook.service
[Unit]
Description=GitHub Webhook Server
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/webhook
Environment="WEBHOOK_SECRET=your_secret_here"
ExecStart=/usr/bin/python3 /var/www/webhook/webhook.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl enable webhook
sudo systemctl start webhook
sudo systemctl status webhook
Bước 4: Expose qua Nginx
Webhook server đang nghe trên 127.0.0.1:9000. Thêm location vào Nginx config để GitHub có thể gọi được:
location /webhook {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
sudo nginx -t && sudo systemctl reload nginx
Bước 5: Cấu hình GitHub Webhook
Vào repo GitHub → Settings → Webhooks → Add webhook. Điền các thông tin:
- Payload URL:
https://yourserver.com/webhook - Content type:
application/json - Secret: Chuỗi ngẫu nhiên — phải khớp với
WEBHOOK_SECRETtrong systemd service - Events: Chọn Just the push event là đủ
Để tạo secret an toàn:
openssl rand -hex 32
Kiểm tra hoạt động
GitHub có nút Redeliver trong tab Webhooks để gửi lại request test — tiện hơn là push một commit rác chỉ để thử. Hoặc push một commit nhỏ lên nhánh main rồi theo dõi:
tail -f /var/log/deploy.log
Thấy dòng “Deploy completed ✓” là xong. Nếu không thấy, log của systemd thường chỉ thẳng lỗi ở đâu:
journalctl -u webhook -f
Các lỗi hay gặp và cách fix
403 Forbidden — Secret key không khớp. Kiểm tra lại WEBHOOK_SECRET trong systemd service có giống với secret điền trên GitHub không. Sửa xong nhớ reload:
sudo systemctl daemon-reload && sudo systemctl restart webhook
Permission denied khi chạy git pull — User www-data không có quyền với thư mục app. Fix một lệnh:
sudo chown -R www-data:www-data /var/www/myapp
Git hỏi password — Server chưa cấu hình SSH key với GitHub. Cách nhanh là dùng Personal Access Token trực tiếp trong URL remote:
git remote set-url origin https://[email protected]/username/repo.git
Deploy script timeout — Script bị kill sau 120 giây theo mặc định. Nếu deploy của bạn mất lâu hơn — build Docker image, chạy database migration — tăng giá trị timeout trong subprocess.run() cho phù hợp.
Kết luận
Về bản chất, webhook chỉ là một HTTP server nhỏ ngồi chờ tín hiệu từ GitHub rồi chạy script. Không có gì phức tạp. Nhưng nó cắt đứt cái vòng lặp SSH-pull-restart mà mình (và có lẽ bạn) đã tốn không biết bao nhiêu đêm.
Khi nào cần rollback tự động, deploy nhiều môi trường, hay thông báo Slack khi có sự cố — lúc đó mới tính đến GitHub Actions hoặc các platform CI/CD chuyên dụng. Còn bây giờ, với project cần chạy ngay hôm nay: cách này đủ dùng, ít code, ít thứ có thể vỡ.
