"Push to deploy" is the deployment experience many solo site owners and small teams want most: commit and push locally, and the live site updates automatically — no logging into the server to type commands. In the Git ecosystem there are three common routes to achieve this: GitHub Actions, Git Hooks, and a webhook listener. This article breaks down each one and finishes with guidance on choosing between them.
Option 1: GitHub Actions Auto-Deployment
If "push to deploy" is your goal, the easiest way to standardize it is GitHub Actions. Every push to main triggers a cloud-side build and an rsync to the server. The full workflow looks like this:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy via rsync
uses: easingthemes/ssh-deploy@main
with:
SSH_PRIVATE_KEY: \${{ secrets.SSH_PRIVATE_KEY }}
SOURCE: "dist/"
REMOTE_HOST: \${{ secrets.DEPLOY_HOST }}
REMOTE_USER: \${{ secrets.DEPLOY_USER }}
TARGET: /var/www/example.com/
GitHub Secrets to Configure
All the variables above come from repository secrets, never from code. Add them in the workflow repository under Settings → Secrets and variables → Actions:
| Secret | Description |
|---|---|
| SSH_PRIVATE_KEY | Server SSH private key |
| DEPLOY_HOST | Server IP or domain |
| DEPLOY_USER | SSH username |
The idea is: build and artifact transfer happen in the cloud; the server only receives. Generate the key with a dedicated deploy user — do not reuse the root key everywhere.
After the three secrets are configured, two things still need to be prepared on the server: the public key matching the private key must go into the deploy user's authorized_keys, and the /var/www/example.com directory must be owned by the deploy user. Before the first run, connect manually from your machine with ssh -i ~/.ssh/deploy_key deploy@<host> to confirm passwordless login works, then let Actions take over.
Option 2: Git Hooks (Server-Side)
If you do not want a CI platform, configure a bare repository on the server and let a post-receive hook pull and deploy automatically on push. Here is the server-side hook script:
#!/bin/bash
# /var/repo/site.git/hooks/post-receive
TARGET=/var/www/example.com
GIT_DIR=/var/repo/site.git
while read oldrev newrev ref
do
if [[ $ref =~ main$ ]]; then
echo "Deploying to production..."
git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f
cd $TARGET
npm ci --production
npm run build
sudo systemctl reload nginx
echo "Deployment complete!"
fi
done
The line git --work-tree=... --git-dir=... checkout -f checks out the latest code directly into the target directory — the backbone of the Git Hooks approach.
Server-Side Setup Steps
# 1. Create a bare repository on the server
sudo mkdir -p /var/repo/site.git
cd /var/repo/site.git
sudo git init --bare
# 2. Create the post-receive hook
sudo nano hooks/post-receive
# (paste the script above)
sudo chmod +x hooks/post-receive
# 3. Set permissions on the target directory
sudo mkdir -p /var/www/example.com
sudo chown -R $USER:$USER /var/www/example.com
Local Configuration
# Add the server as a remote
git remote add production ssh://user@your-server/var/repo/site.git
# Push to deploy
git push production main
From then on, every git push production main makes the server run the deploy script automatically — no SSH session needed.
Two pitfalls are common on the local side. First, the server directory must be a bare repository (git init --bare); a normal repository does not accept pushes. Second, the hook file must be executable, or the script will never run even though the push succeeded. Push once with git push production main, then check ls -l hooks/post-receive on the server to confirm both the permission and the content.
Option 3: Webhook-Triggered Deployment
If the server has no GitHub Actions, or you want to trigger from another platform (Gitee, GitLab, etc.), run a lightweight webhook listener on the server:
#!/bin/bash
# deploy-webhook.sh - Webhook listener running on the server
API_PORT=9000
SECRET="your-webhook-secret"
while true; do
request=$(nc -l -p $API_PORT)
# Verify signature and pull the latest code
cd /var/www/example.com
git pull origin main
npm ci --production
npm run build
sudo systemctl reload nginx
done
A word of caution: in production you must verify the webhook request signature (platforms send a signature header); otherwise anyone can trigger your deploy script. For production, Option 1 or 2 is preferable — a self-written listener suits an intranet or personal project better. If you do need to write your own webhook, prefer an off-the-shelf tool (like the open-source webhook project or the built-in webhook support of CI platforms) so signature verification and concurrency protection come from a mature implementation, and you only write the deploy step.
How to Choose Between the Three
| Option | Best For | Pros | Cons |
|---|---|---|---|
| GitHub Actions | Code hosted on GitHub | Cloud build, searchable logs, mature ecosystem | Requires public reach or a runner |
| Git Hooks | Self-hosted Git server | Simple and direct, no extra dependencies | Scripts live on the server; harder to debug |
| Webhook | Multi-platform triggers, custom flows | Flexible, works with any platform | Security and stability are on you |
When choosing, ask three questions: where is the code hosted, can the server be reached publicly, and is the team willing to maintain extra components? For most small and medium projects hosted on GitHub, Option 1 is the most cost-effective starting point.
Security Tips
- Use a dedicated deploy user with minimal permissions — never run deployments as root.
- Add a passphrase to SSH keys and restrict what they can do.
- Clean sensitive files (.env) after deployment so they are never exposed.
- Keep the previous version for rollback — instant rollback when something breaks.
- Add deployment notifications (Slack/email) so failures are known immediately.
- Rotate deployment keys and passwords regularly, and revoke access promptly when a teammate leaves.
- Use
.gitignoreto keep .env and key files out of the repository, preventing accidental commits.
Common Questions
- Push did not trigger a deploy? Check that the hook is executable, the branch name matches (main), and the remote is a bare repository.
- The deploy script cannot write the target directory? Confirm the user running the deploy has write access to /var/www/example.com — use chown or add the user to the right group.
- How do I roll back? Keep the previous version in the target directory (for example, via a symlink switch), or tag releases and checkout the previous tag before re-running the deploy.
- Deploy finishes quickly but the page does not change? Check whether the deploy target directory matches the web server root, and whether a CDN/cache is in front.
- Push times out? Split large repositories with
git push --mirror, or move the build to CI and transfer only the artifacts.
Reference: GitHub Actions documentation https://docs.github.com/actions ; Git Hooks documentation https://git-scm.com/docs/githooks