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.