DeploymentAugust 7, 202612 min read

Git Push to Deploy: How Modern Web App Deployment Actually Works

What actually happens between git push and live traffic: build artifacts, health-gated releases, why a rollback isn't a rebuild, and the four things a deploy pipeline can't do for you.

RThe Runsite Team

You push to main, go and refill the coffee, and by the time you sit down the new version is live. Somewhere in those thirty seconds a machine you have never logged into cloned your repository, worked out how to build it, built it, stored the result, started it next to the version already serving traffic, asked the new one whether it was healthy, moved requests across, and shut the old one down.

Most days you don't need to know any of that. You need it on the day the deploy succeeds and the app breaks anyway. The uploaded avatars have vanished. The migration that ran fine on your laptop has taken the API down. The rollback you assumed would be instant turns out to be a five-minute rebuild that can fail on its own terms. Every one of those surprises lives at a specific point in the pipeline, and they stop being surprising once you can name the point.

So this isn't a walkthrough of connecting a repository. It's the anatomy: what happens, in what order, and which properties of your system fall out of it.

What actually happens between git push and live traffic

Platforms name the steps differently and split them in different places, but the shape is close to universal. Continuous deployment, in the sense hosting platforms use the phrase, means this whole sequence runs on every push to a branch you nominate, without a person in the middle:

  1. A webhook fires. Your Git host, whether that's GitHub, GitLab or Bitbucket, tells the platform that a specific commit landed on a branch it is watching. That commit is the input to everything downstream.
  2. The code is cloned and inspected. The platform fetches the commit and decides how to build it: from a Dockerfile in the repository if there is one, otherwise by detecting the framework from the files it finds.
  3. A build runs in a throwaway environment. Dependencies are installed and assets are compiled, then the result is packaged up. That environment exists for the build and is discarded afterwards.
  4. The result is stored as an immutable artifact. Usually a container image, pushed to a registry. It has an identity of its own, separate from the branch it came from.
  5. A deployment manifest is assembled. Environment variables, secrets, the health check path, resource limits, how many instances to run. Configuration meets the artifact here, and not a moment earlier.
  6. New containers start alongside the old ones. For a stretch of seconds, both versions of your application exist. The old one is still answering every request.
  7. The health gate opens, or it doesn't. The platform waits for the new containers to report themselves ready. If they never do, the release stops here and the old version keeps serving.
  8. Traffic shifts. The router in front of your app starts sending requests to the new containers.
  9. The old containers are drained and stopped. In-flight requests finish, then those containers go away along with everything they had written to disk.

The interesting thing is where your code stops being the unit of work. Up to step four the pipeline cares about a commit. After step four it cares about an image, and the commit is just provenance. Almost everything that follows in this article is a consequence of that handoff.

Build time and run time are two different worlds

The build produces something that has to run in more than one place. The same artifact should be able to boot as a preview environment, as staging, and as production, because that is the only way "we tested this exact thing" is a true statement. Which means the build has to stay ignorant of where it will run.

What belongs in the image is everything identical across environments: your source, the installed dependencies, compiled assets, the runtime itself. What arrives at start time is everything that differs: database connection strings, API credentials, feature flags, the port to listen on. The separation between building, releasing and running, and the rule that configuration lives in the environment rather than the code, are two of the twelve factors described at 12factor.net, and they have aged well enough to be the default assumption of most platforms you'll meet.

dockerfile
# Build time. This value is baked into an image layer. Every
# environment that runs the image gets it, changing it means a
# rebuild, and anyone who can pull the image can read it.
ENV DATABASE_URL=postgres://app:s3cret@db.internal:5432/app

# Run time. The image says nothing about the database. The
# platform injects DATABASE_URL when the container starts, so
# preview and production run the identical artifact.

The practical failure here is quiet. Bake configuration into the image and you no longer have one artifact promoted through environments, you have as many separate builds as you have environments. Staging and production are then two different pieces of software that happen to share a commit hash, and the sentence "it worked in staging" loses most of its meaning. Secrets baked into a layer are the sharper version of the same mistake: image layers are readable by anyone who can pull the image, and a secret committed to a layer stays in that layer even if a later instruction unsets it.

The artifact is the part that matters

A container image is content-addressed. Alongside whatever tag you gave it, it has a digest, a hash of its contents that looks like `sha256:9b2c…`, and that digest refers to exactly those bytes and no others. Tags move. A digest doesn't. As of writing, this is how the OCI image specification defines identity, and it's the property that makes the next paragraph possible.

Because the previous release is still sitting in the registry under its own digest, going back to it is a lookup. The platform assembles a manifest pointing at the old digest, starts those containers, waits for the health gate, moves traffic. No source is fetched, no dependency is resolved, nothing is compiled. It takes about as long as a deploy that skips the build, which is to say seconds.

Compare that with the reflex most developers reach for. `git revert && git push` looks like an undo, but the pipeline treats it as an ordinary new commit: clone, resolve dependencies, build, push, release. You are asking a build to save you from a bad build. It will usually work, and it will occasionally fail for a reason that has nothing to do with your revert, because a package registry is down or a transitive dependency published a new version in the last hour. It is also minutes rather than seconds, and those minutes are happening while production is broken.

Rollback is a lookup, not a rebuild

If a platform can only rebuild from source, what it calls a rollback is really a new deploy that you hope resembles the old one. The question worth asking before you need the answer: does every release leave behind an addressable artifact, and can I point production at yesterday's without touching my repository?

The release gate: why deploys wait for a health check

A health check on a deploy pipeline is doing a different job from the one on your monitoring dashboard. Monitoring tells you about a system that is already serving. The check in a release is a gate: the new containers get no traffic at all until they answer it, and if they never answer, the traffic never moves. This is why a deploy can fail without any user noticing. The old version was serving the whole time, and the release simply refused to complete.

It also explains a class of confusing incident. If your check only confirms that the HTTP server is listening, then an application that boots with an unreachable database will pass the gate and start returning errors to real users. If the check is honest about the dependencies the app needs to do its job, the release stalls instead and you get a failed deploy rather than an outage.

How the traffic movement itself is staged, and how the checks behind it get tuned, is its own design decision and worth its own article. The principle is enough here.

A health check here is a gate, not a dashboard

Monitoring tells you about a system that is already serving traffic. A release gate decides whether it gets traffic at all. No answer from the new version means no traffic, however good the check would have looked on a graph, and a release isn't finished until that answer arrives.

Three shapes of deploy, and what each one costs you

The pipeline above is the same everywhere, but what comes out the far end isn't. Three shapes cover most of what you'll deploy, and the useful axis between them is what exists between two requests.

ShapeWhat gets deployedBetween requestsWhat to watch
Long-running containerA process, started once and kept aliveThe process stays up, holding connection pools, in-memory caches and background timersWhat the platform does when the app goes quiet, and whether idle costs you money or latency
Static artifact on a CDNA directory of prebuilt files, copied to edge nodesFiles sitting on disk, nothing executingCache invalidation on release, and the point where you need a server after all
Per-request functionA handler the platform invokes on demandNothing, by designStartup cost on the first invocation and any ceiling on execution time
The same pipeline can produce any of these. The difference shows up in your bill and your latency, not in your git history.

The long-running container is what you get from a platform that runs web apps and APIs as containers, and it's the default for anything with a database behind it. Its behaviour when nobody is using it varies enormously between platforms. That is a topic of its own, covered in what cold starts are and why free-tier apps fall asleep. Static output has no such problem because there is nothing to wake up, which is why static site hosting with a preview URL on every pull request is the cheaper answer whenever your build produces plain files. Choosing between a prebuilt site and a server for a given framework is a decision with enough nuance to deserve its own treatment.

Four things git push will not deploy for you

The pipeline covers your application. It does not cover the state your application sits in, and this is where most genuinely bad deploy days come from.

1. Database migrations are a one-way door

During a rolling release, two versions of your application run at the same time, the new containers and the ones still draining, and they talk to the same database. Not two databases, one. So any schema change you ship has to be workable for the code on its way out as well as for the code arriving.

That's the reason a rename breaks production even when the new code is correct. Rename `name` to `full_name` in one migration and the old containers, which still select `name`, start throwing errors the moment the migration lands, and they keep serving a share of your traffic until the release finishes. The standard way around it is to split the change in two and let both versions coexist in between, an approach Martin Fowler describes as ParallelChange and which you'll also see called expand and contract:

sql
-- Expand: add the new column, leave the old one untouched.
ALTER TABLE users ADD COLUMN full_name text;

-- Then deploy code that writes both and reads whichever exists,
-- backfill the new column, and verify. Only in a later release,
-- once nothing reads the old column, do you contract:
ALTER TABLE users DROP COLUMN name;

It's more work than one migration, and it's not always necessary. A short maintenance window makes the whole problem go away, and plenty of applications can afford one. It becomes unavoidable the moment you want releases with no downtime, because that requirement is what puts two versions on the same schema in the first place.

The asymmetry is worth stating plainly: rolling your code back does not roll your schema back. A dropped column stays dropped, and the previous release, pointed at a schema it was never written for, may fail in ways that are harder to diagnose than the bug you were escaping. Who runs the migration and when is your decision to make, but the recovery path is worth deciding before you need it. What a managed database does for you and what stays yours is covered in managed PostgreSQL for developers, and the database itself lives on managed PostgreSQL hosted in the EU.

2. Anything you wrote to local disk

A container's writable layer is tied to that container. When the container goes, the layer goes with it, which is standard container behaviour rather than a platform limitation, and Docker's own storage documentation says as much. Persisting data means putting it somewhere outside the container.

The timing is what catches people out. Files written next to the application don't disappear gradually or on some cleanup schedule. They disappear at step nine, the moment a deploy retires the old containers. So an upload feature that writes to `./uploads` works perfectly through development, testing and the first weeks of production, and then loses every file on the day you ship an unrelated bug fix. User uploads belong in S3-compatible object storage from the start, and the practical side of moving them there is in uploading user files to S3.

3. Background jobs and schedules

Your web process gets replaced on every deploy. Whatever else you run does not necessarily follow the same rules. A worker consuming a queue or a task on a schedule has a lifecycle of its own, and unless the platform ties them together, it can be running last week's code against this week's database. Long jobs make it worse, since a task that started before the deploy is still executing the old version after it. Scheduled work runs on cron jobs with their own deploy lifecycle, and it's worth knowing which version of your code is on the other end of a schedule.

4. Domains, DNS, and certificates

The first time you point a custom domain at a platform you are doing a cutover, and the pipeline barely participates. The TTL on your DNS records determines how long resolvers keep handing out the old address, so some visitors reach the new deployment while others are still going to the previous host, and lowering the TTL is only useful if you do it well in advance. Certificates typically follow rather than lead: an ACME issuer has to verify that you control the domain before it will issue for it, which usually means DNS has to point at the platform first and HTTPS arrives shortly after, not simultaneously. Neither of these is triggered by a push, and both are worth doing on a quiet afternoon.

Preview environments: the cheapest thing on this list

Once the pipeline exists, pointing it at a pull request instead of the main branch costs the platform almost nothing. Same clone, same build, same manifest, different address. What you get back is disproportionate: a reviewer can open the change instead of reading it, and the bugs that only show up with real assets and a real database surface before the merge instead of after.

It also quietly enforces the discipline from earlier. An artifact that can only run in production because its configuration is baked in cannot be spun up as a preview at all. If previews work, your build and your configuration are probably separated properly.

What separates one platform from another

Every platform in this category will tell you it deploys from Git, and that is true and not very informative. The differences show up in the axes below, and each of them corresponds to a specific bad afternoon you can have later. Comparing named platforms against these axes is a separate exercise: the European ones are lined up against them in what to look for in a European Heroku alternative, two named US platforms are put through them in Render vs Railway, and the case where the platform is one you run yourself is in self-hosted PaaS versus managed:

AxisThe question to askWhen it bites
Reproducible buildsDoes the same commit produce the same artifact next month?A rebuild of an old commit fails or behaves differently, and you can't tell why
Addressable artifactIs every release stored, and can I redeploy an old one without rebuilding?You need to be back on yesterday's version in under a minute
Health-gated releaseDoes traffic wait for the new version to report ready?A broken build reaches users instead of failing quietly
Config and secretsAre they attached at release time, or baked into the build?Environments drift apart, and secrets end up readable inside image layers
Preview environmentsDoes a pull request get a running instance with no extra setup?Review happens by reading diffs, and integration bugs land on main
ObservabilityLogs, resource metrics and a deploy history you can correlate?Something broke at some point and nobody can say which release did it
Region and residencyWhere do the containers, build artifacts and logs physically sit?A customer, an auditor or a DPA asks and the honest answer is "several countries"

The last row is the one most easily deferred and hardest to retrofit. Build artifacts and application logs are as much a data location question as the database is, and the reasoning behind keeping the whole stack in one jurisdiction is set out in EU hosting for developers.

How Runsite handles it

Runsite implements the pipeline described above, so it's a reasonable concrete example of the shape. Connect a repository on GitHub, GitLab or Bitbucket and a webhook fires on every push to your configured branch. A Dockerfile is optional: the framework is detected automatically for Next.js, Django, Rails, FastAPI and others, and if you supply your own Dockerfile it is used instead, which is the escape hatch for anything unusual.

The built image is stored in Runsite's internal registry, and the deployment manifest carries environment variables, secrets and health check configuration separately from it. Releases are rolling: new containers start alongside the existing ones, traffic shifts once health checks pass, and a failed check rolls the release back to the previous healthy version rather than putting a broken build in front of users. Health check paths and intervals are configurable. Every pull request gets its own isolated environment on a unique URL without extra configuration. Rollbacks and deploy history are available on the Pro plan (€25/mo as of writing).

As of writing, a deploy takes roughly 30 seconds, with the full clone, build and release cycle typically completing in under a minute. HTTPS is automatic on the runsite.app subdomain or your own domain, and real-time logs, CPU and memory metrics and deploy history are in the dashboard. Everything runs in Frankfurt, Germany, with containers, environment variables, build artifacts and logs staying inside the EU and a signed GDPR data processing agreement on every plan, including the free one. The free tier covers one web service with auto-deploy from Git and free SSL, and active apps don't cold-start. You can deploy web apps straight from Git on EU infrastructure, or start with Static Sites if your build produces plain files instead of a running process. Step-by-step setup lives in the Runsite docs.

The short version

Nine steps run between your push and the first request to the new code, and the pivot is step four, where a commit becomes an artifact. Configuration joins that artifact at release time, not in the build, which is what lets one image run in preview and production. Because the artifact persists, a rollback is a lookup rather than a rebuild, and a platform that can only rebuild from source can't really offer one. The health check gates the release: no answer from the new version, no traffic. And four things stay yours no matter how good the pipeline is, namely your schema, anything written to local disk, your background workers, and your DNS.

The fastest way to internalise all of this is to watch it happen once. Connect a repository to a platform that builds and releases on every push, push a commit, and read the build log while it runs. Everything above is in there, with timestamps.

FAQ

Frequently Asked Questions

Common questions about this service.

A webhook from your Git host tells the platform a commit landed on the branch it watches. The platform clones that commit, works out how to build it from a Dockerfile or by detecting the framework, and runs a build in a throwaway environment. The result is stored as an immutable artifact, usually a container image in a registry. A deployment manifest then attaches environment variables, secrets and the health check configuration to that artifact, new containers start alongside the ones already serving, and traffic only shifts once the new version reports itself healthy. The old containers are drained and stopped last. The key transition is the artifact: after it exists, the pipeline no longer deals in commits.

Point production at the previously built artifact rather than rebuilding from source. Because a container image is addressed by a digest of its contents and the previous release is still in the registry, redeploying it is a lookup: assemble a manifest against the old digest, start the containers, wait for the health check, shift traffic. It takes seconds. Running git revert and pushing looks like an undo but is treated as an ordinary new commit, so it triggers a full clone, dependency resolution and build. That takes minutes while production is broken, and it can fail for unrelated reasons such as a package registry outage. If your platform can only rebuild from source, it does not really support rollback.

That depends on the platform and on how you have wired it, and it is worth confirming rather than assuming. The more important point is that migrations behave differently from code regardless of who triggers them. During a rolling release two versions of your application run against the same database, so a schema change has to work for the outgoing code as well as the incoming code. A rename that drops the old column immediately will break the containers still serving the old version. Splitting the change into expand and contract phases, adding the new column first and removing the old one in a later release, is the usual way around it. Rolling code back does not roll a schema back.

Because they were written to the container's own filesystem. A container's writable layer lives and dies with that container, so anything your application saved next to itself is discarded when the deploy retires the old containers. This is standard container behaviour rather than a platform quirk. The reason it surprises people is the timing: the files survive restarts and weeks of production traffic, then vanish during an unrelated release. User uploads, generated files and anything else that has to outlive a deploy belong in object storage or on a volume that exists independently of the container.

Your app deserves to be online

Free to start. Deploy in under a minute. No credit card needed.