# How Git-Push Deployments Work Under the Hood

> What actually happens between git push and your app being live: webhook delivery, build queues, health checks and the traffic swap, explained step by step.

David Viejo · Published 2026-03-18 · Updated 2026-10-20 · 10 min read

Canonical: https://temps.sh/blog/how-git-push-deployments-work

A git-push deployment is a 7-step pipeline: webhook fires, build queues, repo clones, container builds, health check runs, traffic swaps, old container stops. Every platform that does `git push` to deploy — Vercel, Heroku, Coolify, Dokploy, or a self-hosted tool like [Temps](https://temps.sh/) — runs this same pipeline. The differences are in speed, traffic swap correctness, and how much you configure manually.

Understanding each step cuts debugging time in half. When a deploy fails, it's almost always the build, the health check, or the proxy — and knowing which step lets you go straight to the right log.

> **TL;DR:** Every git-push deployment follows the same 7-step pipeline: webhook, build queue, clone, container build, health check, traffic swap, cleanup. The differences between platforms are in speed, traffic swap correctness, and how much you configure manually. When a deploy fails, it's almost always the build, the health check, or the proxy.

## How do git push deployments work?

When you `git push`, the platform receives a webhook, queues a build, clones the repo at the exact commit SHA, builds a Docker image, health-checks the new container, then flips the proxy to route traffic to it — all within 60–90 seconds on a typical app. The old container stays live until the health check passes, giving you zero-downtime deploys.

## Step 1: The Webhook Fires

When you push to a repository on [GitHub](https://github.com/), [GitLab](https://about.gitlab.com/), or [Gitea](https://about.gitea.com/), the platform sends an HTTP POST to any webhooks registered for that repo. The payload includes the commit SHA, the branch name, the repository URL, and the pusher's identity.

Your deployment platform registers one of these webhooks when you connect a repository. On [Vercel](https://vercel.com/), it happens automatically when you import a project. On [Coolify](https://coolify.io/), the GitHub App registers it; for GitLab or Gitea you [paste Coolify's webhook URL and secret](https://coolify.io/docs/applications/sources/gitlab) into the repository's webhook settings yourself. Temps registers it for every supported provider (GitHub, GitLab, Gitea/Forgejo and Bitbucket).

The webhook request is just an HTTP POST. Your deployment server needs a public IP and an open port to receive it. This is why deployment platforms need to be accessible from the internet, not just from your private network.

One thing worth knowing: GitHub waits 10 seconds for a 2xx response. If your server is slower or down, the delivery is recorded as failed and is [not retried automatically](https://docs.github.com/en/webhooks/using-webhooks/handling-failed-webhook-deliveries), so the push is never built unless someone redelivers it from the webhook settings or the API. Well-built platforms acknowledge immediately, queue the build asynchronously, and deduplicate by commit SHA in case a delivery is redelivered by hand.

## Step 2: The Build Queue

The deployment server receives the webhook, parses the payload, and queues a build job. It stores the commit SHA, the branch, and a pointer to the repository.

Queuing matters because pushes can come faster than builds complete. If two developers push within 30 seconds of each other, the second push should wait for the first build to finish (or cancel it, depending on the platform's configuration). Naively triggering a concurrent build for every push causes resource contention and race conditions on the traffic swap.

Platforms handle this differently: Vercel runs builds in parallel on separate infrastructure. Coolify queues them on your server. The right behavior depends on whether you have enough build capacity to run concurrently.

> **On Temps:** when a newer deployment starts for the same environment, Temps cancels the in-flight one and marks it "Superseded by a newer deployment for this environment", so an obsolete build never finishes ahead of the new commit. [How to cancel stale deployments automatically](https://temps.sh/blog/how-to-cancel-stale-deployments-automatically) covers the same pattern in GitHub Actions and GitLab CI.

## Step 3: Clone and Detect

The build agent clones the repository at the specific commit SHA. This is always a specific SHA, not just the branch head, because the branch might advance between when the webhook fired and when the build starts.

After cloning, the build system detects how to build the app. There are two common approaches:

**Dockerfile detection.** If a `Dockerfile` is present at the root, use it. The developer has already specified the build process. This is the most explicit option and the least surprising.

**Buildpacks.** If there's no Dockerfile, [Cloud Native Buildpacks](https://buildpacks.io/) (CNB) scan the repository for language indicators: a `package.json` suggests Node.js, a `requirements.txt` suggests Python, a `go.mod` suggests Go. The matching buildpack downloads the right runtime, installs dependencies, and produces a container image. [Heroku](https://www.heroku.com/) pioneered this model; the CNB specification standardized it. [Nixpacks](https://nixpacks.com/docs/getting-started), [Railway](https://railway.com/)'s former builder, took a similar detect-and-build approach with Nix-based builds; it's now in maintenance mode, succeeded by Railpack.

The advantage of buildpacks: you push code without a Dockerfile and the platform figures it out. The downside: if your app needs something non-standard, the buildpack's defaults might not be right, and debugging why takes longer than just writing a Dockerfile.

## Step 4: The Container Build

The actual build runs inside [Docker](https://www.docker.com/) (or a Docker-compatible builder like [BuildKit](https://github.com/moby/buildkit)). For a Node.js app, this means `npm ci` followed by your build command. For a Python app, `pip install`. For a Go binary, `go build`.

Build logs stream in real time to the dashboard. This is worth appreciating: you're watching exactly what would happen if you ran `docker build` locally. If the build fails because a dependency version is missing or an environment variable is undefined, the error is right there in the log.

A detail that matters for build speed: **Docker layer caching.** A well-structured Dockerfile copies `package.json` and runs `npm install` before copying the rest of the source code. That way, the installed dependencies layer gets cached between builds, and only the application code layer gets rebuilt on each push. A poorly structured Dockerfile invalidates the cache on every build. The difference is 30 seconds versus 4 minutes for a typical Node.js app.

## Step 5: Health Check Before Traffic Switch

Before the new container gets any production traffic, most deployment platforms run a health check. The new container starts, and the platform pings a health endpoint and waits for a 200 response.

This step is what makes zero-downtime deployment possible. The old container keeps serving requests while the new one warms up. If the new container never passes its health check (because the new code has a startup bug, or the database migration failed, or a required environment variable is missing), it never receives traffic. The old version stays live.

Without a health check, the platform would just replace the old container with the new one and hope. Sometimes it works. Sometimes users get 502 errors for 10–30 seconds while the new container cold-starts.

> **On Temps:** after the new container starts, Temps sends HTTP `GET` requests to `/` (or the `health.path` set in `.temps.yaml`) and needs **2 consecutive successful responses** before routing traffic to it. Connection errors are retried until the **startup timeout** (300 seconds); if the app hasn't passed by then, or keeps answering with error responses for 60 seconds in a row, the deployment fails and the old container keeps serving. The check runs once, at deploy time. [How health checks work](https://temps.sh/docs/deployments#how-do-health-checks-work) has the full rules.

## Step 6: The Traffic Swap

Once the new container passes its health check, the proxy routes new requests to it. The mechanism varies by platform:

**Nginx-based platforms** update an upstream block and run `nginx -s reload`. The reload is graceful: old worker processes finish their in-flight requests before exiting. The cost is that every swap means rewriting config and reloading, which is easy to get wrong in a DIY script.

**[Traefik](https://traefik.io/traefik/)** (used by Coolify and Dokploy) supports dynamic configuration: it picks up the new container via Docker labels without restarting. In-flight requests on the old container are generally handled gracefully, though the behavior depends on Traefik's version and configuration.

**Edge networks** (Vercel, Cloudflare) route traffic via their global infrastructure with connection draining behavior, ensuring in-flight requests complete on the old version before it's removed.

**Pingora** (used by Temps) is the same proxy technology that powers Cloudflare's network — it's open-source and written in Rust. Temps uses Pingora as its reverse proxy, with atomic route table updates so the traffic switch happens in a single operation, with no config file to rewrite or reload.

The key distinction is between "stop sending new requests to old container" and "wait for old requests to finish before stopping the old container." The second is harder to implement correctly, but it's the difference between zero-downtime and almost-zero-downtime.

## Step 7: Cleanup

After traffic moves to the new container, the old container stops. Container images from old deploys get retained for a configurable period (to support rollbacks) and then pruned. Build artifacts get cleaned up.

This cleanup step is easy to neglect in a DIY setup and causes a subtle problem: if you're running frequent deploys, old Docker images accumulate and fill your disk. Platforms handle this automatically; a bare Docker setup needs a cron job running `docker system prune`.

> **On Temps:** a [rollback](https://temps.sh/docs/rollbacks) is manual. You pick an earlier deployment in the dashboard (or pass its ID to the CLI), and Temps creates a new deployment from it, reusing its Docker image when that image is still on the server and rebuilding its commit when it isn't. Nothing rolls back on its own after a deploy has gone live.

## Platform comparison: git-push deploy pipeline

| Feature                           | Vercel              | Coolify                                                 | Temps                                                        |
| --------------------------------- | ------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
| Webhook auto-registration         | Yes (GitHub/GitLab) | GitHub App: yes; GitLab/Gitea: add the webhook manually | Yes (GitHub, GitLab, Gitea/Forgejo, Bitbucket)               |
| Build infrastructure              | Managed cloud       | Your server                                             | Your server                                                  |
| Stale deploy cancellation         | Yes                 | Manual                                                  | Automatic                                                    |
| Health check before traffic swap  | Yes                 | Configurable                                            | Yes (2 consecutive, 5 min default startup timeout)           |
| Proxy technology                  | Edge CDN            | Traefik                                                 | Pingora (Cloudflare-built, open-source)                      |
| Atomic traffic swap               | Yes                 | Graceful                                                | Yes                                                          |
| Rollback to an earlier deployment | Yes                 | Yes                                                     | Yes (manual; reuses the image when it's still on the server) |
| Self-hosted                       | No                  | Yes                                                     | Yes                                                          |
| Pricing                           | See pricing page    | Free / Coolify Cloud from $5/mo                         | Free self-host / Temps Cloud (managed add-on, coming soon)   |

_Vercel and Coolify cells checked October 2026._

## The Full Pipeline, Summarized

```
git push
  -> webhook fires (HTTP POST to your deployment server)
  -> build queued (deduplicated by SHA, stale builds cancelled)
  -> repository cloned at commit SHA
  -> build detection (Dockerfile or buildpacks)
  -> container build (Docker, logs stream to dashboard)
  -> health check (2 consecutive successes within the startup timeout)
  -> traffic swap (proxy re-routes requests atomically to new container)
  -> old container drains and stops
  -> image cleanup (prior images retained for rollback)
```

Every platform that does git-push deploys runs some version of this same pipeline. If you want to see what the manual version looks like without any platform, we wrote a full walkthrough: [how to deploy Next.js to a VPS the manual way](https://temps.sh/blog/deploy-nextjs-to-vps-manual). For more on the traffic swap specifically, see [how to add zero-downtime deployments to any Docker app](https://temps.sh/blog/how-to-add-zero-downtime-deployments-docker).

## Why the health check step matters most

When a deploy fails, it's almost always at one of three steps: the build (a code error), the health check (a startup bug or missing env var), or the traffic swap (a proxy misconfiguration). The health check is the safety net for all of them.

A health check configured to require only one success can still let a flaky startup through — an app that takes 3 seconds to become healthy and times out 20% of the time will pass a single-success check and fail intermittently in production. Requiring 2 consecutive successes (as Temps does) catches this class of bug before traffic ever hits the new container.

The startup timeout matters too. Errors during startup are retried rather than failing the deploy on the first 500, because many apps need time to warm caches or open database connections before they answer cleanly. But the retries can't go on forever: if the app still hasn't passed when the timeout runs out (5 minutes on Temps; the next release makes it adjustable from 30 seconds to an hour for apps that boot slowly), the deploy fails and the old version keeps serving. A finite window means a broken release is caught before the swap instead of discovered by your users, and the deploy pipeline doesn't hang waiting on it.

## FAQ

### Does git push deployment work with monorepos?

Yes, with platform-specific support. Most platforms let you specify which subdirectory to build from and what the build command is. On Temps, each app in the monorepo is its own project with its own root directory. See [how to deploy a monorepo with Temps](https://temps.sh/blog/deploy-monorepo-with-temps) for a full walkthrough.

### What happens if the build fails halfway through?

The old deployment keeps serving traffic. The failed build is logged, you get a notification (if configured), and nothing is promoted to production. The failed build artifacts are cleaned up.

### Can I skip the health check?

Most platforms let you disable health checks. The trade-off is that failed deployments can reach production — the platform swaps traffic to the new container even if it's not responding. Only disable health checks for deployments where you have another safety mechanism (e.g., a separate smoke test step in your CI pipeline that runs before pushing).

### How is `git push` deployment different from CI/CD?

They're not mutually exclusive. In most teams, CI runs tests on every push, and if tests pass, the CI pipeline triggers a deployment. The `git push` deployment platform handles the "deploy" half: cloning, building, and running the container. The CI platform handles tests, linting, and the decision of whether to promote. Vercel and Temps both integrate with GitHub Actions so you can combine both.

**Related:** [Read the full v0.1.0 release notes](https://temps.sh/blog/temps-0-1-0-release-observe-postgres-ha-autofixer) · [How to add zero-downtime deployments to any Docker app](https://temps.sh/blog/how-to-add-zero-downtime-deployments-docker)

---

Source: https://temps.sh/blog/how-git-push-deployments-work · Index of all pages: https://temps.sh/llms.txt
