![[시리즈 2/5] GitHub + Webhook: Git 푸시로 배포 트리거하기](https://blog.flyerschal.com/uploads/posts/cicd-s2-cover.png)
[시리즈 2/5] GitHub + Webhook: Git 푸시로 배포 트리거하기
[시리즈 2/5] GitHub + Webhook: Git 푸시로 배포 트리거하기

이 글은 "Claude Code로 구축하는 무중단 자동 배포 시스템" 시리즈의 두 번째 글입니다.
- 무중단 자동 배포 시스템이란?
- [현재] GitHub + Webhook: Git 푸시로 배포 트리거하기
- Docker + Blue/Green: 무중단 배포 구현하기
- 텔레그램 알림 + 로그 전략
- Claude Code로 배포 시스템 구축하기
이 글에서 다루는 것
git push를 하면 서버가 자동으로 알아채고 배포를 시작하는 구조를 만듭니다. 핵심은 GitHub Webhook과 adnanh/webhook 서버입니다.
1단계: GitHub 저장소 설정
새 저장소 생성
# 로컬에서 프로젝트 초기화
mkdir my-blog && cd my-blog
git init
# GitHub에서 저장소 생성 후 연결
git remote add origin https://github.com/your-org/my-blog.git
# 첫 커밋 & 푸시
echo "# My Blog" > README.md
git add . && git commit -m "initial commit"
git push -u origin main
Private 저장소 권장
배포 시스템을 구축하면 서버에서 소스코드를 자동으로 clone합니다. 코드 보안을 위해 Private 저장소를 사용하세요.
Private 저장소에서 자동 clone이 가능하려면 인증 수단이 필요합니다. 두 가지 방법이 있습니다.
2단계: 서버 인증 설정
방법 A: GitHub Personal Access Token (PAT)
가장 간단한 방법입니다. 토큰을 URL에 포함시켜 clone합니다.
토큰 생성:
- GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
- Generate new token
- 설정:
- Token name:
deploy-server - Expiration: 90일 (만료 시 갱신 필요)
- Repository access: Only select repositories → 대상 저장소 선택
- Permissions: Contents → Read-only
- Token name:
- Generate token → 토큰 복사
사용법:
# deploy.sh에서 사용
REPO_URL="https://${GITHUB_TOKEN}@github.com/your-org/my-blog.git"
git clone --depth 1 "$REPO_URL" /tmp/build
⚠️ 주의사항
- 토큰은
.env파일에 저장하고.gitignore에 추가- Fine-grained token으로 최소 권한만 부여
- 만료일 설정 필수 (무제한 토큰은 위험)
방법 B: Deploy Key (SSH)
저장소별로 SSH 키를 발급하는 방법입니다. 하나의 저장소에만 접근 가능하므로 보안이 더 강합니다.
# 서버에서 SSH 키 생성
ssh-keygen -t ed25519 -C "deploy@my-server" -f ~/.ssh/deploy_my_blog -N ""
# 공개키 확인
cat ~/.ssh/deploy_my_blog.pub
GitHub에 등록:
- 저장소 → Settings → Deploy keys → Add deploy key
- Title:
production-server - Key: 위에서 출력된 공개키 붙여넣기
- Allow write access: 체크 해제 (읽기만 필요)
SSH config 설정:
# ~/.ssh/config
Host github-my-blog
HostName github.com
User git
IdentityFile ~/.ssh/deploy_my_blog
# deploy.sh에서 사용
git clone git@github-my-blog:your-org/my-blog.git /tmp/build
어떤 방법을 선택할까?
| PAT | Deploy Key | |
|---|---|---|
| 설정 난이도 | 쉬움 | 중간 |
| 보안 범위 | 여러 저장소 가능 | 단일 저장소 |
| 갱신 | 만료 시 수동 | 영구 (삭제 전까지) |
| 추천 대상 | 빠른 설정, 여러 프로젝트 | 높은 보안 요구 |
이 시리즈에서는 PAT 방식을 사용합니다. 여러 프로젝트를 하나의 토큰으로 관리할 수 있어 편리합니다.
3단계: Webhook 서버 구축
adnanh/webhook이란?
adnanh/webhook은 Go로 작성된 경량 Webhook 서버입니다. HTTP 요청을 받아 쉘 명령어를 실행합니다.
왜 adnanh/webhook인가?
- 단일 바이너리, 의존성 없음
- JSON으로 간단한 설정
- HMAC 검증 내장
- Docker 이미지 제공
Docker Compose로 Webhook 서버 실행
# ~/Documents/services/blog/docker-compose.yml
services:
webhook:
build: ./webhook
container_name: blog-webhook
restart: unless-stopped
env_file:
- .env.production
ports:
- "9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- .:/repo
- ../nginx/conf.d:/nginx-conf
networks:
- blog-net
⚠️ docker.sock 마운트 주의 Webhook 컨테이너가 Docker 명령어를 실행하려면
docker.sock을 마운트해야 합니다. 이는 보안 위험이 있으므로, 신뢰할 수 있는 스크립트만 실행하세요.
Webhook 컨테이너 Dockerfile
# ~/Documents/services/blog/webhook/Dockerfile
FROM almir/webhook
COPY hooks.json /etc/hooks/hooks.json
# Docker CLI 설치 (컨테이너 안에서 docker 명령 실행용)
RUN apk add --no-cache docker-cli curl git
4단계: hooks.json 설정
hooks.json은 Webhook 서버의 핵심 설정 파일입니다. 어떤 요청을 받아서 어떤 명령을 실행할지 정의합니다.
기본 구조
[
{
"id": "blog-deploy",
"execute-command": "/repo/deploy.sh",
"command-working-directory": "/repo",
"pass-arguments-to-command": [
{
"source": "payload",
"name": "head_commit.id"
},
{
"source": "payload",
"name": "head_commit.message"
}
],
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hmac-sha256",
"secret": "YOUR_WEBHOOK_SECRET",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]
각 필드 상세 설명
id: Webhook URL 경로를 결정합니다.
"id": "blog-deploy"→http://localhost:9000/hooks/blog-deploy
execute-command: 트리거 시 실행할 스크립트 경로입니다.
pass-arguments-to-command: GitHub payload에서 값을 추출하여 스크립트 인자로 전달합니다.
head_commit.id— 커밋 해시 (예:abc1234)head_commit.message— 커밋 메시지
trigger-rule: 요청을 검증하는 규칙입니다.
HMAC-SHA256 검증 이해하기
GitHub는 Webhook 요청을 보낼 때 HMAC-SHA256 서명을 포함합니다:
X-Hub-Signature-256: sha256=abc123...
이 서명은 Secret + Payload Body로 생성됩니다. 서버에서 같은 Secret으로 서명을 다시 계산하여 일치하는지 확인합니다.
왜 중요한가?
- 누군가 가짜 Webhook 요청을 보내도 Secret을 모르면 서명이 불일치
- 중간에 payload가 변조되어도 서명이 불일치
- 따라서 반드시 설정해야 합니다
브랜치 필터
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
main 브랜치 푸시만 배포를 트리거합니다. feature 브랜치나 태그 푸시에는 반응하지 않습니다.
5단계: GitHub Webhook 등록
설정 순서
- GitHub 저장소 → Settings → Webhooks → Add webhook
- 입력:
| 항목 | 값 |
|---|---|
| Payload URL | https://your-domain.com/webhook |
| Content type | application/json |
| Secret | hooks.json의 secret과 동일한 값 |
| SSL verification | Enable SSL verification |
| Events | Just the push event |
- Add webhook 클릭
Payload URL 주의사항
Webhook URL은 외부에서 접근 가능해야 합니다:
- ✅
https://blog.flyerschal.com/webhook - ❌
http://localhost:9000/hooks/blog-deploy(외부에서 접근 불가) - ❌
http://192.168.1.100:9000/hooks/...(내부 IP)
6단계: Nginx에서 Webhook 프록시
Webhook 서버는 내부 포트(9000)에서 실행됩니다. 외부에서 HTTPS로 접근하려면 Nginx 프록시가 필요합니다.
# /etc/nginx/conf.d/blog.conf 또는 서비스 nginx 설정
server {
listen 443 ssl;
server_name blog.your-domain.com;
# SSL 설정 (생략)
# Webhook 프록시
location /webhook {
proxy_pass http://blog-webhook:9000/hooks/blog-deploy;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Webhook은 응답 시간이 길 수 있음 (빌드 중)
proxy_read_timeout 300s;
}
}
URL 매핑 관계
GitHub POST → https://blog.your-domain.com/webhook
│
Nginx (443 SSL)
│
proxy_pass → http://blog-webhook:9000/hooks/blog-deploy
│
adnanh/webhook
│
execute-command → /repo/deploy.sh
7단계: 테스트
로컬에서 Webhook 테스트
서버에서 직접 Webhook을 호출하여 테스트합니다:
# Secret 없이 단순 테스트
curl -X POST http://localhost:9000/hooks/blog-deploy \
-H "Content-Type: application/json" \
-d '{"ref":"refs/heads/main","head_commit":{"id":"test123","message":"test"}}'
GitHub에서 테스트
- 빈 커밋으로 테스트:
git commit --allow-empty -m "test: webhook trigger"
git push origin main
- Webhook 전송 결과 확인:
- GitHub → Settings → Webhooks → Recent Deliveries
- 녹색 체크: 성공 (200 OK)
- 빨간 X: 실패 (응답 코드와 body 확인)
자주 발생하는 문제
| 증상 | 원인 | 해결 |
|---|---|---|
| 302 응답 | HTTPS 리다이렉트 | Payload URL에 https:// 사용 |
| 403 응답 | Secret 불일치 | hooks.json과 GitHub 설정의 Secret 확인 |
| 504 응답 | 타임아웃 | Nginx proxy_read_timeout 늘리기 |
| 연결 실패 | 방화벽 | 포트 열기 또는 Nginx 프록시 확인 |
| 빌드는 되는데 배포 안 됨 | 권한 문제 | docker.sock 마운트, 실행 권한 확인 |
전체 디렉토리 구조 (현재까지)
~/Documents/services/blog/
├── docker-compose.yml # blog-blue, blog-green, webhook 서비스
├── .env.production # 환경변수 (Secret, Token 등)
├── deploy.sh # 배포 스크립트 (다음 글에서 작성)
└── webhook/
├── Dockerfile # webhook 컨테이너 빌드
└── hooks.json # Webhook 규칙 정의
이 단계에서 완성된 것
✅ GitHub 저장소 설정 및 인증 (PAT) ✅ adnanh/webhook 서버 구축 (Docker) ✅ hooks.json 설정 (HMAC-SHA256 검증 + 브랜치 필터) ✅ Nginx Webhook 프록시 설정 ✅ 테스트 및 트러블슈팅
다음 글에서는 Webhook이 트리거하는 deploy.sh를 작성하고, Docker 이미지 빌드부터 Blue/Green 전환까지 무중단 배포의 핵심을 구현합니다.

