Tutorial

How to Deploy from GitHub to Server: 3 Methods Compared (SSH, Webhooks, Actions)

June 05, 2026

Back to Blog
Managing servers the hard way? Panelica gives you isolated hosting, built-in Docker and AI-assisted management.
Start free

Quick Answer: 3 Ways to Deploy from GitHub to a Server

There are three practical methods for deploying from GitHub to a production or staging server, each suited to different team sizes, security requirements, and infrastructure complexity:

  • Method 1: Bare repo + post-receive hook - Push directly to a bare Git repo on your server. The hook runs your deploy script. Fastest setup, zero external dependencies.
  • Method 2: GitHub webhook + server endpoint - GitHub calls a URL on your server when you push to a branch. Your server handles the deploy. No GitHub-side runner required.
  • Method 3: GitHub Actions + SSH/rsync - A GitHub-managed runner builds, tests, and deploys your code over SSH. Full CI/CD with logs, secrets management, and workflow control.

The right method depends on whether you need CI (testing, building) before deploying, how many environments you manage, and whether you can expose an inbound webhook endpoint.

Method 1: Bare Repo and Post-Receive Hook (Push to Deploy)

This is the oldest and simplest deployment method. You create a bare Git repository on your server, configure it as a remote in your local repo, and push to it. A post-receive hook runs automatically after each push.

Setup on the Server

mkdir -p /var/repos/myapp.git
cd /var/repos/myapp.git
git init --bare

# Create the deploy hook
nano hooks/post-receive

Hook content:

#!/bin/bash
while read oldrev newrev refname; do
  branch=$(git rev-parse --abbrev-ref $refname)
  if [ "$branch" = "main" ]; then
    GIT_WORK_TREE=/var/www/myapp git checkout -f main
    cd /var/www/myapp
    composer install --no-dev --optimize-autoloader
    php artisan migrate --force
    php artisan config:cache
    echo "Deployed $(git rev-parse --short main)"
  fi
done
chmod +x hooks/post-receive

Setup on Your Local Machine

git remote add production [email protected]:/var/repos/myapp.git

# Deploy by pushing to it
git push production main

How It Works

When you push to the bare repo, Git runs the post-receive hook with the old revision, new revision, and ref name for each updated branch. The hook checks out the working tree to your web root using GIT_WORK_TREE, then runs any post-deploy commands (migrations, cache clear, dependency install).

Pros and Cons

  • Pro: No GitHub account needed for the server. Works with any Git remote (GitLab, Gitea, Bitbucket).
  • Pro: Zero latency - deploy runs immediately on push.
  • Pro: Completely self-contained. No external services involved.
  • Con: No build step. If your project requires compilation (TypeScript, Go, etc.), you need dev tools installed on the server.
  • Con: No test gate. You can push broken code and it deploys immediately.
  • Con: Rollback is manual (git push with a previous commit hash).

Method 2: GitHub Webhook and Server Endpoint

In this approach, GitHub calls an HTTP endpoint on your server whenever a push event occurs. Your server receives the webhook payload, validates it, and runs the deploy script. No runner needed on GitHub's side.

Create the Webhook Receiver

A simple webhook receiver in Python (Flask), listening on your server:

from flask import Flask, request, abort
import hmac
import hashlib
import subprocess
import os

app = Flask(__name__)
SECRET = os.environ.get("WEBHOOK_SECRET", "change-me")

def verify_signature(payload, signature):
    expected = "sha256=" + hmac.new(
        SECRET.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route("/deploy", methods=["POST"])
def deploy():
    signature = request.headers.get("X-Hub-Signature-256", "")
    if not verify_signature(request.data, signature):
        abort(401)
    payload = request.get_json()
    branch = payload.get("ref", "").split("/")[-1]
    if branch == "main":
        result = subprocess.run(
            ["/opt/deploy/deploy.sh"],
            capture_output=True, text=True, timeout=120
        )
        return f"Deployed: {result.stdout}", 200
    return "No action", 200

Configure GitHub Webhook

In your GitHub repository, go to Settings, then Webhooks, then Add webhook:

  • Payload URL: https://yourserver.com/deploy
  • Content type: application/json
  • Secret: your WEBHOOK_SECRET value
  • Events: Just the push event

The Deploy Script

#!/bin/bash
set -e
cd /var/www/myapp
git fetch origin
git reset --hard origin/main
npm ci
npm run build
composer install --no-dev
php artisan migrate --force
php artisan config:cache
echo "Deploy complete at $(date)"

Pros and Cons

  • Pro: Deploy triggers automatically on push, no manual action needed.
  • Pro: Signature validation (HMAC-SHA256) prevents unauthorized triggers.
  • Pro: Works without GitHub Actions minutes; self-hosted compute.
  • Con: Your server needs a public HTTPS endpoint reachable by GitHub. Not suitable for servers behind NAT without a tunnel.
  • Con: Still no test gate by default (you can add it in the deploy script, but it runs on your server).
  • Con: Webhook failures are silent unless you check GitHub's delivery history.

Method 3: GitHub Actions and SSH/rsync

GitHub Actions runs a workflow in a hosted or self-hosted runner. After building and testing, it connects to your server over SSH and transfers files with rsync or runs a remote deploy command. This is the full CI/CD approach.

Workflow File (.github/workflows/deploy.yml)

name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install and build
        run: |
          npm ci
          npm run build

      - name: Run tests
        run: npm test

      - name: Deploy via rsync
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          SERVER_HOST: ${{ secrets.SERVER_HOST }}
          SERVER_USER: ${{ secrets.SERVER_USER }}
        run: |
          eval $(ssh-agent -s)
          echo "$SSH_PRIVATE_KEY" | ssh-add -
          rsync -avz --delete \
            --exclude='.git' \
            --exclude='node_modules' \
            --exclude='.env' \
            -e "ssh -o StrictHostKeyChecking=no" \
            ./dist/ $SERVER_USER@$SERVER_HOST:/var/www/myapp/

      - name: Post-deploy commands
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          SERVER_HOST: ${{ secrets.SERVER_HOST }}
          SERVER_USER: ${{ secrets.SERVER_USER }}
        run: |
          eval $(ssh-agent -s)
          echo "$SSH_PRIVATE_KEY" | ssh-add -
          ssh -o StrictHostKeyChecking=no $SERVER_USER@$SERVER_HOST \
            "cd /var/www/myapp && php artisan migrate --force && php artisan config:cache"

Set Up Secrets in GitHub

In your repository, go to Settings, then Secrets and variables, then Actions. Add:

  • SSH_PRIVATE_KEY - The private key for a deploy user on your server.
  • SERVER_HOST - Your server IP or hostname.
  • SERVER_USER - The SSH username (ideally a dedicated deploy user, not root).

Pros and Cons

  • Pro: Full CI/CD - build, test, and deploy in a single workflow. Broken code never reaches production.
  • Pro: Workflow logs are stored in GitHub. Easy to audit what deployed when.
  • Pro: Rich ecosystem of reusable actions.
  • Pro: Works even if your server is behind a firewall - it initiates outbound SSH.
  • Con: Depends on GitHub Actions availability and your minutes quota.
  • Con: SSH key management requires careful rotation practices.
  • Con: More moving parts - runner, secrets, workflow YAML, remote commands.

Comparison Table

Aspect Bare Repo + Hook GitHub Webhook GitHub Actions
Setup time 15-30 minutes 30-60 minutes 1-3 hours
Requires public endpoint No Yes No
Build step support On your server On your server On GitHub runner
Test gate before deploy No Optional Yes (native)
Rollback mechanism Manual git push Manual git push Re-run or revert PR
CI integration None Partial Full
GitHub dependency Low Medium High
Audit trail Git log only GitHub delivery history Full workflow logs

Decision Matrix: Which Method Fits Your Situation?

  • Solo developer, simple PHP or static site, no build step: Bare repo + post-receive hook. Fastest to set up, no external dependencies.
  • Small team, automatic deploys, server has public IP: GitHub webhook + server endpoint. Self-contained, event-driven, no CI minutes consumed.
  • Team with tests, TypeScript frontend, need deploy history and rollback visibility: GitHub Actions. The test gate alone is worth the added complexity.
  • Server behind NAT or firewall, no public inbound access: Either bare repo (you push) or GitHub Actions (runner initiates outbound SSH). Webhook method requires a public endpoint.
  • Multiple environments (staging + production): GitHub Actions with environment-specific secrets and branch protection rules for production.

Security Considerations

SSH Key Management

Regardless of which method you use, the deploy SSH key is the critical security boundary:

  • Use a dedicated deploy user with a dedicated SSH key. Do not reuse your personal key.
  • Restrict the deploy user to specific commands using command= in authorized_keys.
# /home/deploy/.ssh/authorized_keys - restrict to deploy script only
command="/opt/deploy/deploy.sh",no-port-forwarding,no-X11-forwarding ssh-ed25519 AAAA... deploy@ci

Webhook Signature Validation

Always validate the X-Hub-Signature-256 header using HMAC-SHA256. A webhook endpoint without signature validation is effectively an unauthenticated remote code execution endpoint - anyone who finds the URL can trigger a deploy.

Secret Rotation

Rotate SSH deploy keys and webhook secrets regularly (quarterly is a reasonable baseline) and immediately when team members with access leave the organization.

Least Privilege for the Deploy User

The deploy user should own the deploy target directory but have no sudo access, no access to other users' home directories, and no ability to modify system configuration.

TL;DR

  • Bare repo + post-receive hook: simplest, fastest, no external dependencies, no test gate.
  • GitHub webhook: automatic, self-hosted, requires a public inbound endpoint, no CI minutes used.
  • GitHub Actions: full CI/CD with test gate, workflow logs, and rollback visibility; server needs outbound SSH only.
  • For solo developers and simple sites, start with Method 1. For teams that need a test gate, go straight to Method 3.
  • Always use a dedicated deploy SSH key, validate webhook signatures, and apply least-privilege to your deploy user.

Panelica includes a built-in Git Manager that connects directly to your repositories. Set a deploy branch, configure deploy hooks, and push-to-deploy without configuring separate webhook endpoints or managing SSH keys manually. Git-powered deployments are part of the core panel, not a plugin.

Related Posts

Security-first hosting panel

Hosting management, the modern way.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
Migration from Plesk, made simple.