Next.js 프로젝트 GitHub Webhook 기반 CI/CD 구성
Proxmox 컨테이너에서 GitHub webhook을 활용한 Next.js 프로젝트 자동 배포 가이드
Next.js 프로젝트 GitHub Webhook 기반 CI/CD 구성
GitHub webhook을 활용하여 Next.js 프로젝트를 Proxmox 컨테이너에 자동으로 배포하는 완전한 CI/CD 파이프라인을 구축하는 방법을 알아보겠습니다.
이 가이드를 통해 배울 내용
- Proxmox 컨테이너 기반 개발 환경 구성
- GitHub webhook을 활용한 자동 배포 시스템 구축
- Node.js/Express 기반 webhook 서버 구현
- PM2를 활용한 프로세스 관리
- 보안을 고려한 배포 환경 설정
시스템 아키텍처 개요
배포 플로우
GitHub push → Webhook → 컨테이너 → 자동 빌드 및 배포
- 개발자가 GitHub에 코드를 푸시
- GitHub가 설정된 webhook URL로 이벤트 전송
- Proxmox 컨테이너 내 webhook 서버가 이벤트 수신
- 자동으로 최신 코드를 pull하고 빌드/배포 실행
요구사항
사전 준비사항
- Proxmox VE 환경
- GitHub 저장소 (Next.js 프로젝트)
- 기본적인 Linux/Docker 지식
- 도메인 또는 고정 IP (webhook 수신용)
Proxmox 컨테이너 생성 및 기본 설정
먼저 Proxmox에서 Ubuntu LXC 컨테이너를 생성하고 기본 환경을 설정합니다.
{`# Proxmox 웹 인터페이스에서 새 CT 생성- Template: ubuntu-22.04-standard- CPU: 2 cores- Memory: 2048 MB- Storage: 20 GB- Network: DHCP 또는 고정 IP 설정컨테이너 시작 후 접속pct enter <container-id>`}
컨테이너 준비 완료
기본 환경 설정이 완료되었습니다. 이제 프로젝트 배포를 위한 디렉터리를 준비하겠습니다.
프로젝트 디렉터리 구조 설정
배포용 디렉터리 구조를 생성하고 권한을 설정합니다.
{`# 배포용 사용자 생성
useradd -m -s /bin/bash deployer
usermod -aG sudo deployer프로젝트 디렉터리 생성mkdir -p /var/www/nextjs-app
chown -R deployer:deployer /var/www/nextjs-app로그 디렉터리 생성mkdir -p /var/log/webhook
chown -R deployer:deployer /var/log/webhookSSH 키 생성 (GitHub 접근용)su - deployer
ssh-keygen -t rsa -b 4096 -C "your-email@example.com"
cat ~/.ssh/id_rsa.pub # 이 키를 GitHub Deploy Keys에 등록`}
SSH 키 등록
생성된 공개 키를 GitHub 저장소의 Settings > Deploy keys에 등록해야 합니다. 읽기 전용 권한으로 설정하세요.
Webhook 서버 구현
GitHub webhook을 수신하고 배포를 실행하는 Express 서버를 구현합니다.
{`cd /var/www
mkdir webhook-server
cd webhook-serverpackage.json 초기화npm init -y필요한 패키지 설치npm install express crypto child_process fs path`}
{`const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');const app = express();
const PORT = 3001;// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';// JSON 파싱 미들웨어
app.use('/webhook', express.json());// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[\const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;timestamp}] \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');exec(`bash \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;error.message}`);
reject(error);
return;
}log(`Deploy stdout: \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;stdout}`);
if (stderr) log(`Deploy stderr: \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;stderr}`);resolve(stdout);
});
});
}// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}const event = req.headers['x-github-event'];// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;event} ignored`);
res.status(200).send('Event ignored');
}} catch (error) {
log(`Webhook error: \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;error.message}`);
res.status(500).send('Internal server error');
}
});// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});app.listen(PORT, () => {
log(`Webhook server started on port \const express = require('express');
const crypto = require('crypto');
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3001;
// GitHub webhook secret (환경변수로 설정)
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'your-webhook-secret';
const PROJECT_PATH = '/var/www/nextjs-app';
const REPO_URL = 'git@github.com:your-username/your-repo.git';
// JSON 파싱 미들웨어
app.use('/webhook', express.json());
// 로그 함수
function log(message) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${message}\n`;
console.log(logMessage);
fs.appendFileSync('/var/log/webhook/deploy.log', logMessage);
}
// GitHub signature 검증
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = 'sha256=' + hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
// 배포 스크립트 실행
function deployApp() {
return new Promise((resolve, reject) => {
const deployScript = path.join(__dirname, 'deploy.sh');
exec(`bash ${deployScript}`, (error, stdout, stderr) => {
if (error) {
log(`Deploy error: ${error.message}`);
reject(error);
return;
}
log(`Deploy stdout: ${stdout}`);
if (stderr) log(`Deploy stderr: ${stderr}`);
resolve(stdout);
});
});
}
// Webhook 엔드포인트
app.post('/webhook', async (req, res) => {
try {
const signature = req.headers['x-hub-signature-256'];
const payload = JSON.stringify(req.body);
// Signature 검증
if (!verifySignature(payload, signature)) {
log('Invalid signature');
return res.status(401).send('Unauthorized');
}
const event = req.headers['x-github-event'];
// push 이벤트만 처리
if (event === 'push') {
const branch = req.body.ref.split('/').pop();
// main 브랜치만 배포
if (branch === 'main') {
log('Received push to main branch, starting deployment...');
try {
await deployApp();
log('Deployment completed successfully');
res.status(200).send('Deployment triggered');
} catch (error) {
log(`Deployment failed: ${error.message}`);
res.status(500).send('Deployment failed');
}
} else {
log(`Push to ${branch} branch ignored`);
res.status(200).send('Branch ignored');
}
} else {
log(`Event ${event} ignored`);
res.status(200).send('Event ignored');
}
} catch (error) {
log(`Webhook error: ${error.message}`);
res.status(500).send('Internal server error');
}
});
// 헬스체크 엔드포인트
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
log(`Webhook server started on port ${PORT}`);
});#123;PORT}`);
});`}
배포 스크립트 작성
실제 배포 작업을 수행하는 bash 스크립트를 작성합니다.
{`#!/bin/bash환경 변수PROJECT_PATH="/var/www/nextjs-app"
REPO_URL="git@github.com:your-username/your-repo.git"
APP_NAME="nextjs-app"echo "Starting deployment process..."프로젝트 디렉터리가 없으면 클론if [ ! -d "$PROJECT_PATH" ]; then
echo "Cloning repository..."
git clone $REPO_URL $PROJECT_PATH
cd $PROJECT_PATH
else
echo "Pulling latest changes..."
cd $PROJECT_PATH
git fetch origin
git reset --hard origin/main
fi의존성 설치echo "Installing dependencies..."
npm ci --production=false빌드 실행echo "Building application..."
npm run buildPM2로 애플리케이션 재시작echo "Restarting application with PM2..."
if pm2 list | grep -q $APP_NAME; then
pm2 restart $APP_NAME
else
pm2 start npm --name $APP_NAME -- start
pm2 save
fi빌드 결과 확인if [ $? -eq 0 ]; then
echo "Deployment completed successfully!"디스크 공간 정리 (선택사항)npm prune이전 빌드 파일 정리 (선택사항)find .next -name "*.map" -delete 2>/dev/null || trueexit 0
else
echo "Deployment failed!"
exit 1
fi`}
{`chmod +x /var/www/webhook-server/deploy.shwebhook 서버 소유권 변경chown -R deployer:deployer /var/www/webhook-server`}
PM2 설정 및 Nginx 리버스 프록시 구성
PM2로 webhook 서버를 관리하고, Nginx로 리버스 프록시를 설정합니다.
{module.exports = { apps: [ { name: 'webhook-server', script: 'server.js', cwd: '/var/www/webhook-server', user: 'deployer', env: { NODE_ENV: 'production', PORT: 3001, WEBHOOK_SECRET: 'your-super-secret-webhook-key' }, error_file: '/var/log/webhook/webhook-error.log', out_file: '/var/log/webhook/webhook-out.log', log_file: '/var/log/webhook/webhook-combined.log', time: true, max_restarts: 5, restart_delay: 1000 } ] };}
{`# deployer 사용자로 전환
su - deployercd /var/www/webhook-serverPM2로 애플리케이션 시작pm2 start ecosystem.config.jsPM2 목록 확인pm2 list시스템 부팅 시 자동 시작 설정pm2 startup
pm2 save`}
GitHub Webhook 설정
GitHub 저장소에서 webhook을 설정하여 컨테이너로 이벤트를 전송하도록 구성합니다.
{`1. GitHub 저장소 > Settings > Webhooks 이동
2. "Add webhook" 클릭
3. 다음 정보 입력:
Payload URL: http://your-domain.com/webhook
Content type: application/json
Secret: your-super-secret-webhook-key (서버 설정과 동일)
Which events: Just the push event
Active: 체크
"Add webhook" 클릭하여 저장`}
보안 고려사항
- webhook secret은 복잡하고 예측하기 어려운 값으로 설정하세요
- 가능하다면 HTTPS를 사용하여 SSL/TLS 암호화를 적용하세요
- 방화벽에서 필요한 포트만 열어두세요 (80, 443)
SSL/TLS 인증서 설정 (권장)
HTTPS를 적용하는 방법은 두 가지가 있습니다. Nginx Proxy Manager를 사용하면 GUI로 쉽게 관리할 수 있습니다.
SSL 설정 방법 선택
- Nginx Proxy Manager (권장): GUI 기반으로 쉬운 SSL 인증서 관리 - NPM 가이드 보기
- 직접 Certbot 사용: CLI 기반 수동 설정 (아래 참조)
Nginx Proxy Manager로 SSL 설정
GUI 기반으로 쉽게 SSL 인증서를 관리할 수 있습니다
Proxmox 환경에서는 Nginx Proxy Manager를 별도 컨테이너로 구성하여 여러 서비스의 SSL을 중앙에서 관리하는 것을 권장합니다.
설정 단계:
- Nginx Proxy Manager 가이드에 따라 NPM 설치
- NPM 웹 인터페이스에서 프록시 호스트 추가
- Let's Encrypt SSL 인증서 자동 발급
- webhook URL을
https://your-domain.com/webhook
으로 업데이트
장점:
- GUI 기반 쉬운 관리
- 자동 SSL 인증서 갱신
- 여러 서비스 중앙 관리
- 실시간 로그 모니터링
HTTPS 설정 완료
이제 webhook URL을 https://your-domain.com/webhook
으로 업데이트하세요.
모니터링 및 로그 관리
배포 프로세스를 모니터링하고 로그를 관리하는 스크립트를 작성합니다.
{`#!/bin/bashecho "=== Webhook Server Status ==="
pm2 show webhook-serverecho -e "\n=== Last 20 Deployment Logs ==="
tail -n 20 /var/log/webhook/deploy.logecho -e "\n=== System Resources ==="
df -h /var/www
free -hecho -e "\n=== Active Processes ==="
pm2 list`}
{/var/log/webhook/*.log { daily missingok rotate 30 compress delaycompress notifempty copytruncate postrotate pm2 reloadLogs endscript }}
테스트 및 검증
배포 테스트
설정이 완료되면 다음과 같이 테스트해보세요:
수동 테스트
{`# 헬스체크 확인
curl http://your-domain.com/health수동 배포 실행cd /var/www/webhook-server
bash deploy.shPM2 상태 확인pm2 status`}
GitHub Push 테스트
{`# 로컬에서 테스트 변경사항 생성
echo "// Test deployment" >> README.md
git add .
git commit -m "Test: webhook deployment"
git push origin main서버에서 로그 확인tail -f /var/log/webhook/deploy.log`}
문제 해결
Webhook이 받아지지 않는 경우:
- 방화벽 설정 확인
- Nginx 설정 및 프록시 확인
- GitHub webhook 설정의 URL 및 secret 확인
배포가 실패하는 경우:
- SSH 키 권한 확인
- Node.js 버전 호환성 확인
- 디스크 공간 확인
PM2 서비스가 재시작되지 않는 경우:
- 권한 설정 확인
- 로그 파일 경로 확인
- 환경 변수 설정 확인
추가 개선사항
고급 기능 추가
더 안정적인 CI/CD 파이프라인을 위한 개선사항
1. 백업 및 롤백 시스템
- 배포 전 현재 버전 백업
- 실패 시 이전 버전으로 자동 롤백
2. 알림 시스템
- Slack/Discord 배포 알림
- 이메일 오류 알림
3. 멀티 환경 지원
- staging/production 환경 분리
- 브랜치별 다른 배포 전략
4. 성능 모니터링
- 애플리케이션 성능 메트릭 수집
- 리소스 사용량 모니터링
참고 자료
배포 완료!
축하합니다! GitHub webhook 기반 Next.js CI/CD 파이프라인이 성공적으로 구성되었습니다.
이제 main
브랜치에 코드를 푸시할 때마다 자동으로 배포가 실행됩니다.