How to Set Up Automatic Deploys on Git Push
The thing you actually miss when you leave a managed platform is not the server — it is the magic of git push and watching your change appear live a minute later. Good news: that workflow is not magic, and you can rebuild it on your own VPS in an afternoon. It is a Git webhook, a small listener, and a build script. This guide explains how each piece fits, how to secure it properly (the part most tutorials skip), and where the sharp edges are.
How a Push-to-Deploy Pipeline Works
The whole mechanism is one HTTP request. When you push to GitHub, GitLab, or Bitbucket, the provider sends a webhook — a POST request to a URL you configure — describing what happened: which branch, which commit, who pushed. Your server runs a small endpoint that receives that POST and, if it's a push to the branch you deploy from, kicks off a rebuild. The flow is always the same four beats:
- 1.You
git pushto your main branch. - 2.The Git host sends a signed POST to your webhook URL.
- 3.Your listener verifies the signature and checks the branch.
- 4.It pulls the new code, rebuilds, and restarts the app.
That is push-to-deploy. Everything else is reliability and security around those four steps.
Step 1: Configure the Webhook
On GitHub, the setting lives under Settings → Webhooks → Add webhook for the repository. You provide three things: the payload URL (your endpoint, e.g. https://deploy.yourdomain.com/hooks/myapp), the content type (application/json), and a secret. The secret is not optional, and we'll use it in the next step. Set the trigger to "Just the push event" unless you have a reason to listen for more.
GitLab (Settings → Webhooks) and Bitbucket (Repository settings → Webhooks) work the same way; the only difference is the header name and signature scheme they use, which matters in the next step.
Step 2: Verify the Signature (Do Not Skip This)
Your webhook URL is public. Without verification, anyone who guesses it can trigger a deploy — or worse, if your handler does anything with the payload, feed it crafted data. The fix is the shared secret. GitHub signs every payload with HMAC-SHA256 using that secret and sends the result in the X-Hub-Signature-256 header. Your listener computes the same HMAC over the raw body and compares — using a constant-time comparison, not ==:
import crypto from "crypto"
function verify(rawBody, header, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret)
.update(rawBody)
.digest("hex")
return crypto.timingSafeEqual(
Buffer.from(header), Buffer.from(expected)
)
}Compute the HMAC over the raw request body, before any JSON parsing reformats it — a single whitespace difference makes the signatures mismatch. Reject any request that fails this check with a 401 and never run the deploy.
Step 3: The Build-and-Restart Script
Once a request is verified and you've confirmed it's a push to your deploy branch, the actual work is a short shell script. For a Docker-based app it is roughly:
#!/usr/bin/env bash
set -euo pipefail
cd /srv/apps/myapp
git pull --ff-only origin main
docker build -t myapp:latest .
docker stop myapp || true
docker rm myapp || true
docker run -d --name myapp --env-file .env \
-p 8000:8000 myapp:latestset -euo pipefail is doing real work here: it makes the script abort on the first error instead of charging ahead and restarting with a half-built image. The --ff-only flag on the pull means a force-push or diverged history fails loudly rather than creating a merge commit on your server.
There is a brief gap between docker stop and the new container accepting traffic. For a hobby app that's fine. To close that gap you start the new container first, wait for its health check to pass, then switch the proxy and stop the old one — which is exactly the kind of orchestration that gets tedious to maintain by hand.
The Three Pitfalls Nobody Warns You About
Concurrent pushes. If two pushes land seconds apart, two builds can run at once and stomp on each other. Guard the deploy with a lock file or a single-worker queue so builds run one at a time.
The webhook times out. Git hosts expect a quick response and will mark the delivery failed if your endpoint blocks for the whole build. Respond 200 OK immediately and run the build in the background, then report status separately.
A failed build takes the site down. If the build breaks but the script has already stopped the old container, you're offline. Build the new image first, and only stop the old container once the new one is confirmed healthy — never the other way around.
The Managed Version, on Your Own Server
Every one of those pitfalls is solvable, and once you've solved them you've basically rebuilt a small deploy platform. That is what AODE is. When you enable auto-deploy, AODE generates a per-project webhook secret and verifies every delivery's signature — you add the webhook to your repo settings once, and every push from then on deploys automatically.
Each push produces a new versioned Docker image (myapp:v12, v13, …) with the full build log captured, and if a build fails you can roll back to the last good version in one click instead of SSH-ing in at 2 a.m. You get the git push-and-it-deploys feeling without owning the listener, the lock file, and the restart script.
Try AODE
One-time purchase, lifetime license, 14-day money-back guarantee. Auto-deploy on push, versioned builds, and one-click rollback on your own VPS.
Related Articles
Last updated: June 2026