DeployWise
GitHubWebhooksAuto DeployCI/CD

GitHub Webhooks for Auto Deploy

Automatically deploy your Node.js application to production whenever you push to GitHub using webhooks.

Published: February 28, 2026
9 min read
Updated: 2026

What Are GitHub Webhooks?

GitHub webhooks allow GitHub to send HTTP POST requests to your server whenever certain events occur in your repository. This enables real-time automation like automatic deployments whenever you push code.

The flow: You push code → GitHub sends webhook → Your server receives event → Deployment script runs → Application updates automatically.

Setting Up a Webhook Endpoint

Step 1: Create a Webhook Receiver Endpoint

Create a simple Express endpoint that GitHub can POST to. This will be called whenever events occur.

javascript
import express from 'express';
import crypto from 'crypto';
import { execSync } from 'child_process';

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;

app.post('/api/deploy', (req, res) => {
  // Verify webhook signature (see next section)
  const signature = req.headers['x-hub-signature-256'];
  const payload = JSON.stringify(req.body);

  const hash = 'sha256=' + crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  if (signature !== hash) {
    console.error('Webhook signature verification failed');
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // Only deploy on push events
  if (req.headers['x-github-event'] !== 'push') {
    return res.status(200).json({ message: 'Ignoring non-push event' });
  }

  const branch = req.body.ref.split('/').pop();

  // Only deploy from main branch
  if (branch !== 'main') {
    return res.status(200).json({ message: 'Ignoring non-main branch' });
  }

  console.log('Deploying from branch:', branch);

  try {
    // Execute deployment script
    execSync('bash /home/user/deploy.sh', { stdio: 'inherit' });
    return res.status(200).json({ message: 'Deployment started' });
  } catch (error) {
    console.error('Deployment failed:', error);
    return res.status(500).json({ error: 'Deployment failed' });
  }
});

app.listen(3001, () => {
  console.log('Webhook receiver listening on port 3001');
});

Step 2: Configure Environment Variables

Store your webhook secret in environment variables for security.

bash
# .env.production
GITHUB_WEBHOOK_SECRET=your_webhook_secret_here_min_32_chars
NODE_ENV=production

Step 3: Expose Endpoint Publicly

GitHub needs to reach your endpoint. Use ngrok for local testing or deploy to a public server.

bash
# Local testing with ngrok
npm install -g ngrok
ngrok http 3001

# You'll get a URL like: https://abc123.ngrok.io
# Webhook URL would be: https://abc123.ngrok.io/api/deploy

# Or use a reverse proxy on your server
# Configure Nginx to forward requests to your webhook endpoint
location /api/deploy {
  proxy_pass http://localhost:3001;
}

Verifying Webhook Signatures

GitHub signs every webhook with HMAC-SHA256. Always verify signatures to ensure webhooks are truly from GitHub.

javascript
import crypto from 'crypto';

// Middleware to verify GitHub webhook signature
const verifyWebhookSignature = (req, res, next) => {
  const signature = req.headers['x-hub-signature-256'];
  const githubSecret = process.env.GITHUB_WEBHOOK_SECRET;

  if (!signature || !githubSecret) {
    return res.status(403).json({ error: 'Missing signature or secret' });
  }

  // Get raw request body
  const payload = req.rawBody || JSON.stringify(req.body);

  // Calculate expected signature
  const hash = 'sha256=' + crypto
    .createHmac('sha256', githubSecret)
    .update(payload)
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(hash)
  );

  if (!isValid) {
    console.warn('Invalid webhook signature', { signature, hash });
    return res.status(401).json({ error: 'Invalid signature' });
  }

  next();
};

// Use middleware on webhook route
app.post('/api/deploy', verifyWebhookSignature, (req, res) => {
  // Safe to process webhook
  console.log('Webhook verified and processing...');
});

// Important: Express raw body middleware (required for signature verification)
app.use(express.raw({ type: 'application/json' }));
app.use((req, res, next) => {
  req.rawBody = req.body;
  express.json()(req, res, next);
});

Triggering Deploys on Push Events

Understanding GitHub Webhook Events

GitHub sends different event types. Check the event type header to determine what action to take.

javascript
// Handle different GitHub events
app.post('/api/deploy', verifyWebhookSignature, (req, res) => {
  const eventType = req.headers['x-github-event'];
  const payload = req.body;

  console.log('Received GitHub event:', eventType);

  // Only handle push events
  if (eventType !== 'push') {
    console.log('Ignoring event type:', eventType);
    return res.status(200).json({ message: 'Event ignored' });
  }

  // Extract branch and commit info
  const ref = payload.ref; // e.g., "refs/heads/main"
  const branch = ref.split('/').pop(); // "main"
  const commits = payload.commits;
  const pushedBy = payload.pusher.name;

  console.log({
    branch,
    commitsCount: commits.length,
    pushedBy,
    latestCommit: commits[0]?.message
  });

  // Trigger deployment
  triggerDeployment(branch);

  res.status(200).json({ message: 'Deployment triggered' });
});

Complete Webhook Handler

javascript
const triggerDeployment = async (branch) => {
  console.log('Starting deployment from branch:', branch);

  try {
    // Pull latest code
    console.log('Pulling code...');
    execSync('git pull origin ' + branch);

    // Install dependencies
    console.log('Installing dependencies...');
    execSync('npm ci --production');

    // Run tests
    console.log('Running tests...');
    execSync('npm test');

    // Reload with PM2
    console.log('Reloading application...');
    execSync('pm2 reload app.js');

    // Verify health
    await new Promise(resolve => setTimeout(resolve, 2000));
    const health = execSync('curl -f http://localhost:3000/health').toString();
    console.log('Health check passed:', health);

    console.log('Deployment completed successfully!');
  } catch (error) {
    console.error('Deployment failed:', error.message);

    // Rollback on failure
    console.log('Rolling back to previous version...');
    execSync('git reset --hard HEAD~1');
    execSync('npm ci --production');
    execSync('pm2 reload app.js');

    throw error;
  }
};

Branch Filtering

Only deploy from specific branches (usually main or production). Prevent accidental deployments from feature branches.

javascript
app.post('/api/deploy', verifyWebhookSignature, (req, res) => {
  const eventType = req.headers['x-github-event'];

  if (eventType !== 'push') {
    return res.status(200).json({ message: 'Ignoring non-push event' });
  }

  const ref = req.body.ref;
  const branch = ref.split('/').pop();

  // List of branches that trigger deployment
  const allowedBranches = ['main', 'production'];

  if (!allowedBranches.includes(branch)) {
    console.log('Branch not in deploy list:', branch);
    return res.status(200).json({
      message: 'Branch ignored',
      branch,
      allowedBranches
    });
  }

  console.log('Branch allowed, triggering deployment:', branch);
  triggerDeployment(branch);

  res.status(200).json({
    message: 'Deployment triggered',
    branch
  });
});

Deployment Script

Create a standalone bash script that handles the complete deployment pipeline.

bash
#!/bin/bash
set -e

# Deploy script for webhook triggers
# This script is called by the webhook handler

PROJECT_DIR="/home/user/myapp"
LOG_FILE="/home/user/deploy.log"
SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"

log() {
  echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

notify_slack() {
  local message=$1
  local color=$2

  curl -X POST "$SLACK_WEBHOOK"     -H 'Content-Type: application/json'     -d "{
      "attachments": [{
        "color": "$color",
        "title": "Deployment Update",
        "text": "$message",
        "ts": $(date +%s)
      }]
    }"
}

# Change to project directory
cd "$PROJECT_DIR"

log "=== Deployment started ==="

# Pull latest code
log "Pulling latest code..."
git fetch origin main
git reset --hard origin/main

# Install dependencies
log "Installing dependencies..."
npm ci --production

# Run tests
log "Running tests..."
if ! npm test; then
  log "Tests failed, aborting deployment"
  notify_slack "Deployment failed: Tests did not pass" "danger"
  exit 1
fi

# Reload application
log "Reloading application with PM2..."
pm2 reload app.js

# Wait for processes to start
sleep 3

# Health check
log "Performing health checks..."
for i in {1..5}; do
  if curl -f http://localhost:3000/health > /dev/null; then
    log "Health check passed"
    break
  fi
  if [ $i -eq 5 ]; then
    log "Health check failed after 5 attempts"
    notify_slack "Deployment failed: Health check failed" "danger"
    exit 1
  fi
  sleep 2
done

log "=== Deployment completed successfully ==="
notify_slack "Deployment successful" "good"

Security Best Practices

Always verify webhook signatures

Use HMAC-SHA256 verification to ensure webhooks come from GitHub. Never skip signature verification.

Use environment variables for secrets

Never hardcode webhook secrets. Store in .env or secret management system.

Limit webhook endpoint exposure

Use a firewall to allow GitHub's IP addresses only. Check GitHub's documentation for current IP ranges.

Filter by branch and event type

Only deploy from specific branches. Ignore event types you don't need to handle.

Log all deployments

Keep detailed logs of all webhook triggers and deployments for auditing and debugging.

Use timing-safe comparison

Always use crypto.timingSafeEqual() for signature verification to prevent timing attacks.

Monitor webhook delivery

Check GitHub's webhook delivery logs regularly to ensure webhooks are being delivered successfully.

Testing Webhooks

1. GitHub UI Webhook Testing

GitHub allows you to manually trigger webhooks from the repository settings for testing.

bash
# In your GitHub repository:
1. Go to Settings → Webhooks
2. Find your webhook in the list
3. Click it to edit
4. Scroll down to "Recent Deliveries"
5. Click "Redeliver" to send a test webhook
6. Check the response and see request/response details

2. Local Testing with Ngrok

Use ngrok to expose your local development server to GitHub.

bash
# Terminal 1: Start your Node.js app
npm start

# Terminal 2: Expose with ngrok
ngrok http 3000

# Terminal 3: Configure webhook in GitHub
# Settings → Webhooks → Add webhook
# Payload URL: https://your-ngrok-url.ngrok.io/api/deploy
# Content type: application/json
# Secret: your_webhook_secret
# Events: Push events

# Then test by pushing to your repo
git add .
git commit -m "Test webhook"
git push origin main

# Watch the deployment happen!

3. Manual Webhook Simulation

Test your webhook endpoint by sending a manual POST request with proper signature.

javascript
import crypto from 'crypto';

const payload = {
  ref: 'refs/heads/main',
  repository: { name: 'myapp' },
  pusher: { name: 'testuser' },
  commits: [{ message: 'Test commit' }]
};

const secret = 'your_webhook_secret';
const payloadString = JSON.stringify(payload);
const signature = 'sha256=' + crypto
  .createHmac('sha256', secret)
  .update(payloadString)
  .digest('hex');

fetch('https://your-domain.com/api/deploy', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Hub-Signature-256': signature,
    'X-GitHub-Event': 'push'
  },
  body: payloadString
})
  .then(res => res.json())
  .then(data => console.log('Response:', data))
  .catch(err => console.error('Error:', err));

How DeployWise Auto-Deploy Works

DeployWise simplifies webhook-based deployments with automatic setup and management:

  • Automatically creates and configures GitHub webhook for you
  • Generates secure webhook secret and stores it safely
  • Verifies all webhook signatures using HMAC-SHA256
  • Supports branch filtering and event filtering
  • Provides detailed logs and delivery status
  • Automatically rolls back on deployment failures
  • Sends notifications to Slack on deployment events
Setup Auto-Deploy with DeployWise

Related Guides

Automate deployments with GitHub webhooks

Let DeployWise handle webhook setup, verification, and automatic deployments on every push.