Skip to content
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. Dibahas praktis dengan konteks Radian Group,
4 minutes to read
4 bulan lalu
Zainul Fanani
Deployment Butler: Assistant Deployment yang Jaga Server 24/7
๐Ÿ“… 8 Mar 2026๐Ÿค0 ๐Ÿ‘ 0 ๐Ÿ”— 0

๐Ÿ“Ž 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

text
[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:

python
#!/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:

bash
#!/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:

javascript
// Express.js example
app.get('/health', (req, res) => {
  res.json({ 
    status: 'ok', 
    timestamp: new Date().toISOString(),
    version: process.env.npm_package_version
  });
});
python
# 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:

ini
[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:

bash
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:
bash
export GITHUB_WEBHOOK_SECRET="your-secret-here"

Step 6: Manual Deployment Command

scripts/deployment/deploy-manual.sh:

bash
#!/bin/bash
# Manual deployment trigger

echo "๐Ÿš€ Triggering manual deployment..."
bash scripts/deployment/deploy.sh

Deployment Status Check

scripts/deployment/status.sh:

bash
#!/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:

text
[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:

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

Failed + Rollback:

text
[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

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

Database Migrations

bash
# Run migrations before deployment
npm run migrate

# If migration fails โ†’ abort deployment

Canary Deployment

bash
# 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)

Newsletter

Subscribe to Newsletter

Artikel baru, automation notes, dan engineering insight. Clean inbox, no spam.

Dengan subscribe, kamu setuju menerima update seperlunya.

F
Zainul Fanani

Founder, Radian Group. Engineering & tech enthusiast.

Lihat profil lengkap โ†’