GitHub Webhook으로 자동 배포 구성하기
이 글에서 다루는 내용
코드를
main브랜치에 push하면 자동으로 서버에 배포되는 환경을 구성한다. 별도의 CI/CD 플랫폼 없이 GitHub Webhook과 자체 서버만으로 구현하는 방식이다.
전체 흐름은 다음과 같다.
GitHub Push (main 브랜치)
→ GitHub이 Webhook으로 POST 요청 전송
→ https://ytylab.com/webhook
→ nginx가 내부 서버(192.168.0.164:9000)로 프록시
→ Node.js 수신 서버에서 Secret 검증
→ deploy.sh 실행 (git pull → npm install → build → pm2 restart)
사전 준비
- 도메인 및 HTTPS 인증서 (Let's Encrypt 등)
- nginx 설치된 서버
- Node.js, PM2 설치된 배포 대상 서버
- GitHub 레포지토리
1. GitHub Webhook 설정
GitHub 레포지토리 페이지에서 아래 경로로 이동한다.
Settings → Webhooks → Add webhook
| 항목 | 값 |
|---|---|
| Payload URL | https://ytylab.com/webhook |
| Content type | application/json |
| Secret | 랜덤 문자열 (서버와 동일하게 설정) |
| Which events | Just the push event |
설정 후 GitHub 측에 초록 체크(✅)가 표시되면 연결 성공이다.
빨간 표시(❌)가 뜨면 서버가 응답하지 못한 것이므로 nginx 및 수신 서버 상태를 확인한다.
2. nginx 설정
외부에서 들어오는 /webhook 경로 요청을 내부 수신 서버로 프록시한다.
# /etc/nginx/sites-available/ytylab.com
location /webhook {
proxy_pass http://192.168.0.164:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
설정 적용:
sudo nginx -t
sudo systemctl reload nginx
3. Webhook 수신 서버 (Node.js)
내부 서버에서 9000 포트로 GitHub의 요청을 수신하고 검증한다.
// /var/www/webhook-server/index.js
const express = require('express')
const crypto = require('crypto')
const { exec } = require('child_process')
const app = express()
app.use(express.json())
const SECRET = process.env.WEBHOOK_SECRET
app.post('/webhook', (req, res) => {
// GitHub이 보낸 서명 추출
const sig = req.headers['x-hub-signature-256']
const hash = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(JSON.stringify(req.body))
.digest('hex')
// 서명 불일치 시 401 반환
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(hash))) {
return res.status(401).send('Unauthorized')
}
const repo = req.body.repository?.name
const branch = req.body.ref?.split('/').pop()
// main 브랜치가 아니면 무시
if (branch !== 'main') return res.send('skip')
// 배포 스크립트 실행
exec(`bash /var/www/deploy.sh ${repo}`, (err, out) => {
if (err) return console.error(err)
console.log(out)
})
res.send('ok')
})
app.listen(9000, () => console.log('webhook listening :9000'))
의존성 설치 및 실행
cd /var/www/webhook-server
npm install express
WEBHOOK_SECRET=your_secret_here node index.js
PM2로 상시 실행:
WEBHOOK_SECRET=your_secret_here pm2 start index.js --name webhook-server
pm2 save
4. 배포 스크립트 (deploy.sh)
레포명을 인자로 받아 해당 디렉토리에서 배포를 수행한다.
#!/bin/bash
# /var/www/deploy.sh
APP=$1
cd /var/www/$APP
git pull origin main
npm install
npm run build
pm2 restart $APP --update-env
echo "[$(date)] Deploy done: $APP"
실행 권한 부여:
chmod +x /var/www/deploy.sh
5. 동작 확인
GitHub Webhook 페이지의 Recent Deliveries 탭에서 마지막 요청의 성공/실패 여부와 응답 내용을 확인할 수 있다.
서버 로그 확인:
pm2 logs webhook-server
9000 포트 수신 확인:
ss -tlnp | grep 9000
이 방식의 단점
실제 운영 환경에서 이 방식을 장기적으로 사용하면 다음과 같은 한계에 부딪힌다.
네트워크 의존성 문제
- 공유기 포트포워딩 구성 시 공인 IP가 변동되면 Webhook URL이 깨진다.
- CGNAT 환경이나 ISP 방화벽에 의해 외부 접근 자체가 차단될 수 있다.
보안 취약점
- 내부 서버 IP와 포트가 외부에 노출되는 구조다.
- Secret 검증이 빠지거나 허술하면 임의의 외부 요청으로 배포가 트리거될 수 있다.
운영 불편함
- 빌드 실패 시 롤백 메커니즘이 없다.
- 배포 중 서비스가 일시 중단될 수 있다 (Zero-downtime 미지원).
- 빌드 로그 관리가 어렵다.
- 서버를 이전하거나 IP가 변경되면 전체 설정을 다시 해야 한다.
확장성 한계
- 멀티 서버 배포, 컨테이너 기반 환경에는 적합하지 않다.
- 테스트, 스테이징, 프로덕션 환경 분리가 어렵다.
최신 트렌드: 더 나은 대안들
GitHub Actions (가장 표준적인 방법)
별도 서버 없이 GitHub 인프라에서 빌드 및 배포까지 처리한다.
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run build
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/my-app
git pull
pm2 restart my-app
GitHub Actions Self-hosted Runner
내부 서버에 Runner를 설치하면 서버가 GitHub에 아웃바운드로 연결하므로 포트 개방이 불필요하다. 사내 네트워크나 온프레미스 환경에 특히 적합하다.
# 서버에서 runner 설치 및 등록
./config.sh --url https://github.com/org/repo --token YOUR_TOKEN
./run.sh
Cloudflare Tunnel
포트포워딩 없이 내부 서버를 안전하게 외부에 노출한다. 공인 IP 변동과 무관하게 안정적으로 동작한다.
cloudflared tunnel --url http://localhost:9000
컨테이너 기반 배포 (현재 업계 표준)
Docker + GitHub Actions + 컨테이너 레지스트리를 조합하는 방식이 현재 가장 널리 사용된다.
Push → GitHub Actions → Docker 이미지 빌드
→ Container Registry (GHCR, ECR 등) Push
→ 서버에서 docker pull + 재시작
Zero-downtime 배포, 롤백, 환경 격리가 모두 자연스럽게 해결된다.
정리
| 방식 | 포트 개방 | 보안 | 확장성 | 난이도 |
|---|---|---|---|---|
| Webhook + 포트포워딩 | 필요 | 보통 | 낮음 | 낮음 |
| Self-hosted Runner | 불필요 | 높음 | 보통 | 보통 |
| Cloudflare Tunnel | 불필요 | 높음 | 보통 | 낮음 |
| GitHub Actions + SSH | 불필요 | 높음 | 높음 | 보통 |
| Docker + Actions | 불필요 | 높음 | 매우 높음 | 높음 |
소규모 개인 프로젝트나 학습 목적이라면 이 글에서 소개한 Webhook 방식으로도 충분히 자동 배포를 경험할 수 있다. 하지만 팀 단위 협업이나 프로덕션 서비스라면 GitHub Actions 또는 Self-hosted Runner로의 전환을 권장한다.