Auto Deploy From Github To Server

Auto Deploy From Github To Server

Reading time1 min
#DevOps#Automation#Cloud#GitHub#CI/CD#Deployment

Streamlining Continuous Delivery: Secure, Zero-Downtime Auto-Deployments from GitHub

Dependency on bloated third-party CI/CD systems is rarely necessary for straightforward server applications. Native auto-deployment, triggered by GitHub push events, reduces operational surface area and increases release cadence—all while retaining full control over infrastructure and security context.


Problem: Manual Deployments Introduce Latency and Risk

Typical cycle:

  • Code changes locally.
  • Developer builds, transfers, restarts manually.
  • One forgotten environment variable or missed migration = downtime or broken prod.

Side effect: Even small releases get delayed; operational drift between git and production isn't always obvious.


Direct Auto-Deployment: Construction

Server Prerequisites

  • Static IP, ports 22 and 443 open.
  • Ubuntu 22.04 LTS (tested).
  • Node.js v18.x (or target application runtime).
  • PM2 v5.x for process management.

Install core tooling:

sudo apt update
sudo apt install -y git nodejs npm
sudo npm install -g pm2

Application Directory Layout:

/var/www/
  └── my-app/
        ├── node_modules/
        ├── logs/
        ├── deploy.sh
        └── ...

Principle: Each deployer gets its own system user, no sudo.

Create deploy user:

sudo adduser --disabled-password --gecos "" deployer
sudo mkdir -p /var/www/my-app && sudo chown deployer:deployer /var/www/my-app

SSH key setup must be limited to your CI/CD system or trusted operator(s).

sudo su - deployer
mkdir -m 700 ~/.ssh
nano ~/.ssh/authorized_keys  # Paste your public key
chmod 600 ~/.ssh/authorized_keys

Note: Never reuse the root user's SSH key for deployment users. Restrict access to /var/www/my-app only.


Deployment Script: Idempotent & Safe

A practical deploy.sh must handle errors, logs, and zero-downtime restarts. Skimp here and you'll debug via SSH at 2 a.m.

#!/bin/bash
APP_DIR="/var/www/my-app"
LOG_FILE="${APP_DIR}/logs/deploy.log"
BRANCH="main"

set -eo pipefail

{
  echo "[DEPLOY] $(date)"
  cd "$APP_DIR" || { echo "ERR: cd $APP_DIR failed"; exit 42; }
  git fetch origin "$BRANCH"
  git reset --hard "origin/${BRANCH}"
  npm ci --audit=false
  pm2 reload ecosystem.config.js --only my-app || pm2 start ecosystem.config.js --only my-app
  echo "[DEPLOY] Success $(date)"
} >> "$LOG_FILE" 2>&1
  • npm ci ensures clean, repeatable installs.
  • PM2 reload assumes an ecosystem.config.js. If not present, pm2 restart index.js is fallback.
  • Script is fully idempotent; repeated pushes don’t destabilize state.

Gotcha: If your deploy includes DB migrations, extend deploy.sh with pre/post hooks and apply transactions carefully.


Webhook Integration: GitHub → Server Endpoint

Manual polling is dead. Let GitHub trigger deployments natively:

  1. Webhook URL: Expose /deploy endpoint over HTTPS.
  2. Secret validation: Protect with HMAC SHA1.

Minimal Node.js webhook receiver, with explicit signature check:

// index.js
const http = require('http');
const crypto = require('crypto');
const { exec } = require('child_process');

const SECRET = process.env.WEBHOOK_SECRET || 'CHANGEME';

function verify(sig, body) {
  const hmac = 'sha1=' + crypto.createHmac('sha1', SECRET).update(body).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(hmac));
  } catch { return false; }
}

http
  .createServer((req, res) => {
    if (req.url === '/deploy' && req.method === 'POST') {
      let body = '';
      req.on('data', chunk => (body += chunk));
      req.on('end', () => {
        const sig = req.headers['x-hub-signature'] || '';
        if (!verify(sig, body)) {
          res.writeHead(403); return res.end('Signature mismatch');
        }
        exec('/var/www/my-app/deploy.sh', (err, stdout, stderr) => {
          if (err) {
            console.error(stderr); res.writeHead(500); return res.end('Failed');
          }
          res.writeHead(200); res.end('OK');
        });
      });
    } else {
      res.writeHead(404); res.end();
    }
  })
  .listen(3000);

Tip: Run under pm2 as a systemd service for reliability; memory leaks in vanilla http listeners are possible in long-lived servers.

Configuring GitHub:

  • Payload URL: https://your-server/deploy
  • Content type: application/json
  • Secret: set and match process.env.WEBHOOK_SECRET on the server.
  • Event: Push to main.

Known issue: Without SSL, GitHub refuses to call webhooks. Use Let’s Encrypt + Nginx as reverse proxy, or mkcert for dev.


Security Controls

  • HTTPS required. Terminate TLS before traffic hits your webhook listener.
  • User permissions. Deployment user locked to app dir, no shell, no sudo.
  • Webhooks. HMAC signatures, never rely on IP allowlisting alone (GitHub's IPs can change).
  • Firewall. Drop external traffic to non-HTTPS ports. Only allow CI/CD systems to connect via SSH.

Note: If using cloud VM, security groups are first line of defense.


Testing Flow

Basic validation matrix:

StepExpected OutcomeLog File
Push to mainWebhook POST received/var/www/my-app/logs/*
DeploysPM2 reload, no downtimePM2 + app's http logs
FailureNon-0 exit code in deploy.shdeploy.log + email alert?

To simulate a failed deployment:

  • Break your npm install (e.g., corrupt package.json)
  • Push, then inspect logs:
npm ERR! Unexpected end of JSON input...
[DEPLOY] ERR: npm ci failed

Production Considerations

  • Zero-downtime: PM2’s reload works if app is stateless. Otherwise, expect brief socket disconnects.
  • Migrations: Use npm run migrate step gated behind a lock.
  • Rollback: No built-in rollback—tag stable releases, or graft reflogs for manual recovery.

Alternative: blue-green deployment using Nginx and versioned subdirectories if strict uptime is required. See nginx docs for upstream switching.


Practical Example: Python/Django

Swap deploy.sh install lines:

pip install --upgrade pipenv
pipenv install --deploy --ignore-pipfile
python manage.py migrate --noinput        
systemctl reload gunicorn

Trade-off: Python wheels sometimes rebuild natively; pin OS dependencies, or use dockerized builds.


Summary

Native auto-deployment from GitHub to your own server, using SSH, webhooks, and a hardened system user, achieves fast, reliable delivery at minimal operational cost. No external CI/CD required. Critical to log every step and trap errors—future debugging depends on it.

For workflows with more than one server or stateful workloads, pipeline logic will shift (e.g., use Ansible, or container orchestrator with rolling deploys). For single-node workloads, above approach is robust. Imperfect, but proven.


For questions about other stacks or nuanced rollout strategies—reach out. Engineering never ends.