GitHub Webhooks - Node.js

Snippet Git

Node.js Express and vanilla implementations for GitHub webhooks with signature verification

Node.js - Express Handler

Express server for handling GitHub webhooks.

import crypto from "crypto";
import express from "express";

const app = express();
const SECRET = "your_secret";

app.use(express.raw({ type: "application/json" }));

app.post("/webhook", (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send("Invalid");
  }

  const event = req.headers["x-github-event"];
  if (event === "push") {
    require("child_process").exec("sh /path/to/deploy.sh");
  }

  res.send("OK");
});

app.listen(3000);

Node.js - Verify Signature

Verify GitHub webhook signature in Node.js.

function verifySignature(req) {
  const signature = req.headers["x-hub-signature-256"];
  const hmac = crypto.createHmac("sha256", SECRET);
  const digest = "sha256=" + hmac.update(req.body).digest("hex");
  return signature === digest;
}

Install & Run:

npm install express
node index.js

# Production
pm2 start index.js

Node.js - Vanilla (No Framework)

HTTP server without Express framework.

import crypto from "crypto";
import http from "http";

const SECRET = "your_secret";

http.createServer((req, res) => {
  if (req.method !== "POST" || req.url !== "/webhook") {
    res.writeHead(404);
    return res.end("Not Found");
  }

  let body = [];
  req.on("data", chunk => body.push(chunk));
  req.on("end", () => {
    const payload = Buffer.concat(body).toString();
    const signature = req.headers["x-hub-signature-256"];

    if (!verifySignature(signature, payload)) {
      res.writeHead(401);
      return res.end("Invalid");
    }

    if (req.headers["x-github-event"] === "push") {
      require("child_process").exec("sh /path/to/deploy.sh");
    }

    res.end("OK");
  });
}).listen(3001);

Node.js - Vanilla Verify Signature

Signature verification for vanilla Node.js.

function verifySignature(signature, payload) {
  if (!signature) return false;

  const hmac = crypto.createHmac("sha256", SECRET);
  const digest = "sha256=" + hmac.update(payload).digest("hex");

  return signature === digest;
}

Run:

# Development
node server.js

# Production
pm2 start server.js

Package.json

Required dependencies for Express version.

{
  "name": "github-webhook",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "express": "^4.18.0"
  }
}

PM2 Setup

Run with PM2 process manager for production.

# Install PM2
npm install -g pm2

# Start application
pm2 start index.js --name github-webhook

# Auto-restart on boot
pm2 startup
pm2 save

# Monitor
pm2 logs github-webhook
pm2 monit

Environment Variables

Store secret securely using environment variables.

# .env file
SECRET=your_webhook_secret_here
PORT=3000
// Load environment variables
import dotenv from 'dotenv';
dotenv.config();

const SECRET = process.env.SECRET;
const PORT = process.env.PORT || 3000;

Docker Deployment

Containerize the Node.js webhook handler.

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]

Build and run:

docker build -t github-webhook-node .
docker run -p 3000:3000 -e SECRET=your_secret github-webhook-node

Nginx Reverse Proxy

Proxy webhook requests through Nginx.

server {
    listen 80;
    server_name webhook.example.com;

    location /webhook {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Systemd Service

Run as a systemd service.

Create /etc/systemd/system/github-webhook.service:

[Unit]
Description=GitHub Webhook Handler
After=network.target

[Service]
Type=simple
User=nodejs
WorkingDirectory=/opt/webhook
ExecStart=/usr/bin/node /opt/webhook/index.js
Restart=on-failure
Environment=SECRET=your_secret

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl enable github-webhook
sudo systemctl start github-webhook