Tech

Deployment Butler: Assistant Deployment yang Jaga Server 24/7

Deploy tanpa was-was. Butler yang monitor deploys, rollback otomatis kalau error, dan kasih notifikasi real-time.

๐Ÿ‘ค Zainul Fanani๐Ÿ“… 8 Maret 2026โฑ 1 min read

๐Ÿ“Ž Source:openclaw-sumopod โ€” view on GitHub & star โญ

Deployment Butler

Automated deployment pipeline: GitHub โ†’ VPS with zero-downtime and instant rollback.

Overview

Manual deployments are error-prone. This automation:

  • Triggers on GitHub webhook (push to main)
  • Auto-pulls latest code
  • Runs health checks
  • Rolls back if deployment fails
  • Notifies status via Telegram

Perfect for: Web apps, APIs, microservices, static sites.

Architecture

[GitHub push] โ†“ [Webhook triggered] โ†“ [Deployment pipeline] 1. Pull latest code 2. Install dependencies 3. Build (if needed) 4. Health check โ†“ [If healthy] โ†’ Switch to new version โ†’ Notify success โ†“ [If failed] โ†’ Automatic rollback โ†’ Notify failure โ†’ Keep previous version

Prerequisites

  • OpenClaw installed
  • VPS with systemd
  • GitHub webhook setup
  • Telegram bot
  • Docker (optional but recommended)

Step 1: Webhook Handler

scripts/deployment/webhook-server.py:

#!/usr/bin/env python3 """ GitHub webhook handler for auto-deployment Usage: python3 webhook-server.py """ from http.server import BaseHTTPRequestHandler, HTTPServer import json import hmac import hashlib import subprocess import os # Config WEBHOOK_SECRET = os.getenv("GITHUB_WEBHOOK_SECRET") REPO_PATH = "/var/www/app" SERVICE_NAME = "myapp" BRANCH = "main" def verify_signature(payload, signature): """Verify GitHub webhook signature""" if not signature: return False sha_name, signature = signature.split('=') mac = hmac.new(WEBHOOK_SECRET.encode(), payload, hashlib.sha256) return hmac.compare_digest(mac.hexdigest(), signature) class WebhookHandler(BaseHTTPRequestHandler): def do_POST(self): content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) # Verify signature signature = self.headers.get('X-Hub-Signature-256') if not verify_signature(post_data, signature): self.send_response(401) self.end_headers() return # Parse payload payload = json.loads(post_data) # Check if push to main if payload.get('ref') == f'refs/heads/{BRANCH}': print(f"๐Ÿš€ Deployment triggered by {payload['pusher']['name']}") # Run deployment result = subprocess.run( ["bash", "scripts/deployment/deploy.sh"], capture_output=True, text=True ) if result.returncode == 0: self.send_response(200) self.end_headers() self.wfile.write(b'{"status": "deployed"}') else: self.send_response(500) self.end_headers() self.wfile.write(b'{"status": "failed"}') else: self.send_response(200) self.end_headers() self.wfile.write(b'{"status": "ignored"}') def log_message(self, format, *args): print(f"[Webhook] {format % args}") def run_server(): server = HTTPServer(('0.0.0.0', 9000), WebhookHandler) print("๐ŸŒ Webhook server running on port 9000") server.serve_forever() if __name__ == "__main__": run_server()

Step 2: Deployment Script

scripts/deployment/deploy.sh:

#!/bin/bash # Zero-downtime deployment with rollback set -e APP_DIR="/var/www/app" BACKUP_DIR="/var/www/backups" SERVICE_NAME="myapp" HEALTH_URL="http://localhost:3000/health" MAX_RETRIES=5 RETRY_DELAY=5 LOG_FILE="/var/log/deployment.log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } notify() { local status="$1" local message="$2" # Telegram notification curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -d "chat_id=${TELEGRAM_CHAT_ID}" \ -d "text=${message}" \ -d "parse_mode=Markdown" > /dev/null } pre_deploy() { log "๐Ÿ“ฆ Starting deployment..." # Create backup backup_name="backup_$(date +%Y%m%d_%H%M%S)" cp -r "$APP_DIR" "$BACKUP_DIR/$backup_name" log "๐Ÿ’พ Backup created: $backup_name" # Store current commit cd "$APP_DIR" git rev-parse HEAD > "$BACKUP_DIR/$backup_name.commit" } deploy() { log "๐Ÿ”„ Pulling latest code..." cd "$APP_DIR" git fetch origin git reset --hard origin/main log "๐Ÿ“ฆ Installing dependencies..." # Install based on project type if [ -f "package.json" ]; then npm ci --production elif [ -f "requirements.txt" ]; then pip install -r requirements.txt elif [ -f "Dockerfile" ]; then docker build -t "$SERVICE_NAME:latest" . fi log "๐Ÿ”ง Running build (if needed)..." if [ -f "package.json" ] && grep -q '"build"' package.json; then npm run build fi } health_check() { log "๐Ÿฅ Running health check..." # Restart service systemctl restart "$SERVICE_NAME" # Wait for service to start sleep 3 # Health check with retries for i in $(seq 1 $MAX_RETRIES); do if curl -sf "$HEALTH_URL" > /dev/null; then log "โœ… Health check passed" return 0 fi log "โณ Retry $i/$MAX_RETRIES..." sleep $RETRY_DELAY done log "โŒ Health check failed" return 1 } rollback() { log "๐Ÿšจ Deployment failed! Rolling back..." # Find latest backup latest_backup=$(ls -t "$BACKUP_DIR" | grep "backup_" | head -1) if [ -z "$latest_backup" ]; then log "โŒ No backup found! Manual intervention needed." notify "error" "๐Ÿšจ *Deployment Failed*\nNo backup available!" exit 1 fi # Restore from backup rm -rf "$APP_DIR" cp -r "$BACKUP_DIR/$latest_backup" "$APP_DIR" # Restart service systemctl restart "$SERVICE_NAME" log "โœ… Rollback complete: $latest_backup" notify "error" "๐Ÿšจ *Deployment Failed*\nRolled back to: $latest_backup" } cleanup() { # Keep only last 10 backups cd "$BACKUP_DIR" ls -t | grep "backup_" | tail -n +11 | xargs -r rm -rf log "๐Ÿงน Old backups cleaned up" } # Main deployment flow main() { pre_deploy if deploy; then if health_check; then log "โœ… Deployment successful!" notify "success" "โœ… *Deployment Successful*\nApp updated to latest version" cleanup else rollback exit 1 fi else rollback exit 1 fi } main

Step 3: Health Check Endpoint

Add to your app:

// Express.js example app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), version: process.env.npm_package_version }); });
# Flask example @app.route('/health') def health(): return jsonify({ 'status': 'ok', 'timestamp': datetime.now().isoformat(), 'version': '1.0.0' })

Step 4: Systemd Service

/etc/systemd/system/myapp.service:

[Unit] Description=My Application After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/www/app ExecStart=/usr/bin/node server.js Restart=always RestartSec=10 Environment=NODE_ENV=production Environment=PORT=3000 [Install] WantedBy=multi-user.target

Enable:

systemctl daemon-reload systemctl enable myapp systemctl start myapp

Step 5: GitHub Webhook Setup

  1. Go to GitHub Repo โ†’ Settings โ†’ Webhooks
  2. Add webhook:
    • Payload URL: http://your-vps:9000/webhook
    • Content type: application/json
    • Secret: Generate random string
    • Events: Just the push event
  3. Set environment variable on VPS:
export GITHUB_WEBHOOK_SECRET="your-secret-here"

Step 6: Manual Deployment Command

scripts/deployment/deploy-manual.sh:

#!/bin/bash # Manual deployment trigger echo "๐Ÿš€ Triggering manual deployment..." bash scripts/deployment/deploy.sh

Deployment Status Check

scripts/deployment/status.sh:

#!/bin/bash # Check deployment status echo "๐Ÿ“Š Deployment Status" echo "===================" # Git info cd /var/www/app echo "๐Ÿ“ฆ Current commit: $(git rev-parse --short HEAD)" echo "๐Ÿ“ Last message: $(git log -1 --pretty=%B)" # Service status echo "" echo "๐Ÿ”ง Service status:" systemctl status myapp --no-pager -l # Health check echo "" echo "๐Ÿฅ Health check:" curl -s http://localhost:3000/health | python3 -m json.tool # Recent deployments echo "" echo "๐Ÿ“œ Recent deployments:" tail -10 /var/log/deployment.log

Example Output

Successful Deployment:

[2026-03-08 14:30:01] ๐Ÿ“ฆ Starting deployment... [2026-03-08 14:30:02] ๐Ÿ’พ Backup created: backup_20260308_143002 [2026-03-08 14:30:05] ๐Ÿ”„ Pulling latest code... [2026-03-08 14:30:08] ๐Ÿ“ฆ Installing dependencies... [2026-03-08 14:30:15] ๐Ÿ”ง Running build... [2026-03-08 14:30:25] ๐Ÿฅ Running health check... [2026-03-08 14:30:28] โœ… Health check passed [2026-03-08 14:30:28] โœ… Deployment successful! [2026-03-08 14:30:29] ๐Ÿงน Old backups cleaned up

Telegram Notification:

โœ… *Deployment Successful* App updated to latest version Commit: a1b2c3d

Failed + Rollback:

[2026-03-08 14:35:10] ๐Ÿฅ Running health check... [2026-03-08 14:35:40] โŒ Health check failed [2026-03-08 14:35:41] ๐Ÿšจ Deployment failed! Rolling back... [2026-03-08 14:35:45] โœ… Rollback complete: backup_20260308_143002

Advanced Features

Blue-Green Deployment

# Deploy to blue instance # Health check # Switch nginx to blue # Keep green as backup

Database Migrations

# Run migrations before deployment npm run migrate # If migration fails โ†’ abort deployment

Canary Deployment

# Deploy to 10% of traffic first # Monitor for 5 minutes # If healthy โ†’ deploy to 100%

Conclusion

You now have automated deployment that:

  • โœ… Deploys on every GitHub push
  • โœ… Runs health checks
  • โœ… Auto-rollback on failure
  • โœ… Sends Telegram notifications
  • โœ… Maintains backups

Next Steps:

  • Add database migration handling
  • Implement blue-green deployment
  • Build deployment analytics

Tutorial created for OpenClaw Sumopod

Ada Pertanyaan? Yuk Ngobrol!

Butuh bantuan setup OpenClaw, konsultasi IT, atau mau diskusi project engineering? Book a call langsung โ€” gratis.

Book a Call โ€” Gratis

via Cal.com โ€ข WITA (UTC+8)

F

Zainul Fanani

Founder, Radian Group. Engineering & tech enthusiast.

Radian Group

Engineering Excellence Across Indonesia

Perusahaan

  • CV Radian Fokus Mandiri โ€” Balikpapan
  • PT UNO Solusi Teknik โ€” Balikpapan
  • PT Reka Formasi Elektrika โ€” Jakarta
  • PT Raya Fokus Solusi โ€” Sidoarjo
ยฉ 2026 Radian Group. All rights reserved.