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 → 컨테이너 → 자동 빌드 및 배포

  1. 개발자가 GitHub에 코드를 푸시
  2. GitHub가 설정된 webhook URL로 이벤트 전송
  3. Proxmox 컨테이너 내 webhook 서버가 이벤트 수신
  4. 자동으로 최신 코드를 pull하고 빌드/배포 실행

요구사항

사전 준비사항
  • Proxmox VE 환경
  • GitHub 저장소 (Next.js 프로젝트)
  • 기본적인 Linux/Docker 지식
  • 도메인 또는 고정 IP (webhook 수신용)
1

Proxmox 컨테이너 생성 및 기본 설정

먼저 Proxmox에서 Ubuntu LXC 컨테이너를 생성하고 기본 환경을 설정합니다.

bashProxmox 컨테이너 생성
{`# Proxmox 웹 인터페이스에서 새 CT 생성- Template: ubuntu-22.04-standard- CPU: 2 cores- Memory: 2048 MB- Storage: 20 GB- Network: DHCP 또는 고정 IP 설정컨테이너 시작 후 접속pct enter <container-id>`}
컨테이너 준비 완료

기본 환경 설정이 완료되었습니다. 이제 프로젝트 배포를 위한 디렉터리를 준비하겠습니다.

2

프로젝트 디렉터리 구조 설정

배포용 디렉터리 구조를 생성하고 권한을 설정합니다.

bash디렉터리 구조 생성
{`# 배포용 사용자 생성
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에 등록해야 합니다. 읽기 전용 권한으로 설정하세요.

3

Webhook 서버 구현

GitHub webhook을 수신하고 배포를 실행하는 Express 서버를 구현합니다.

/var/www/webhook-server/package.jsonbash
{`cd /var/www
mkdir webhook-server
cd webhook-serverpackage.json 초기화npm init -y필요한 패키지 설치npm install express crypto child_process fs path`}
/var/www/webhook-server/server.jsjavascript
{`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}`);
});`}
4

배포 스크립트 작성

실제 배포 작업을 수행하는 bash 스크립트를 작성합니다.

/var/www/webhook-server/deploy.shbash
{`#!/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`}
bash스크립트 실행 권한 부여
{`chmod +x /var/www/webhook-server/deploy.shwebhook 서버 소유권 변경chown -R deployer:deployer /var/www/webhook-server`}
5

PM2 설정 및 Nginx 리버스 프록시 구성

PM2로 webhook 서버를 관리하고, Nginx로 리버스 프록시를 설정합니다.

/var/www/webhook-server/ecosystem.config.jsjson
{module.exports = &#123;   apps: [     &#123;       name: 'webhook-server',       script: 'server.js',       cwd: '/var/www/webhook-server',       user: 'deployer',       env: &#123;         NODE_ENV: 'production',         PORT: 3001,         WEBHOOK_SECRET: 'your-super-secret-webhook-key'       &#125;,       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     &#125;   ] &#125;;}
bashPM2로 서비스 시작
{`# deployer 사용자로 전환
su - deployercd /var/www/webhook-serverPM2로 애플리케이션 시작pm2 start ecosystem.config.jsPM2 목록 확인pm2 list시스템 부팅 시 자동 시작 설정pm2 startup
pm2 save`}
6

GitHub Webhook 설정

GitHub 저장소에서 webhook을 설정하여 컨테이너로 이벤트를 전송하도록 구성합니다.

textGitHub 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)
7

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을 중앙에서 관리하는 것을 권장합니다.

설정 단계:

  1. Nginx Proxy Manager 가이드에 따라 NPM 설치
  2. NPM 웹 인터페이스에서 프록시 호스트 추가
  3. Let's Encrypt SSL 인증서 자동 발급
  4. webhook URL을 https://your-domain.com/webhook으로 업데이트

장점:

  • GUI 기반 쉬운 관리
  • 자동 SSL 인증서 갱신
  • 여러 서비스 중앙 관리
  • 실시간 로그 모니터링
HTTPS 설정 완료

이제 webhook URL을 https://your-domain.com/webhook으로 업데이트하세요.

8

모니터링 및 로그 관리

배포 프로세스를 모니터링하고 로그를 관리하는 스크립트를 작성합니다.

/usr/local/bin/webhook-monitor.shbash
{`#!/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`}
/etc/logrotate.d/webhookbash
{/var/log/webhook/*.log &#123;     daily     missingok     rotate 30     compress     delaycompress     notifempty     copytruncate     postrotate         pm2 reloadLogs     endscript &#125;}

테스트 및 검증

배포 테스트

설정이 완료되면 다음과 같이 테스트해보세요:

1

수동 테스트

bash헬스체크 및 수동 배포 테스트
{`# 헬스체크 확인
curl http://your-domain.com/health수동 배포 실행cd /var/www/webhook-server
bash deploy.shPM2 상태 확인pm2 status`}
2

GitHub Push 테스트

bash테스트 커밋 푸시
{`# 로컬에서 테스트 변경사항 생성
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 브랜치에 코드를 푸시할 때마다 자동으로 배포가 실행됩니다.