Skip to content
Chapter 98Lesson 1

How a git push becomes a deployment

The mental model for shipping to Vercel, where each git push lands and why production is just an alias pointing at one immutable build.

You ended the last chapter with a green CI gate in front of a site nobody can reach yet. “Going live” sounds like a button you press once, but on Vercel it’s a model, and what triggers a deploy is a git push.

This lesson builds that model with no app code. By the end you can look at any git push and predict what Vercel does with it and where the result lands, so the next lesson’s clicks confirm what you already understand.

A common first confusion is “why did my pull request ship to production?” Different git events deploy to different places, and the mapping is exact:

  • A push to main creates a production deployment.
  • A push to any other branch with an open pull request creates a preview deployment: a build with its own generated *.vercel.app URL, posted as a comment on the PR.
  • Running the project locally, with vercel dev or with pnpm dev reading .env.local, is the development environment.

There’s no “promote to staging” ceremony and no separate publish step. Where a commit lands is decided entirely by which branch you pushed to and whether a PR is open.

git event push → main
git event push → feature/* (PR open)
git event vercel dev (local)
deployment Production deployment
deployment Preview deployment *.vercel.app
deployment Development
Each git situation maps to exactly one deployment kind.

One detail carries the rest of the chapter: the code on all three can be byte-for-byte identical. What differs isn’t the code, it’s the environment variables the deployment reads. A preview and production can run the very same build and still behave differently, because one talks to a test Stripe key and the other to a live one. Environment is not code.

Now the reframe at the heart of this lesson. You might picture “the deploy” as an action you take once the code is ready, but it isn’t. Vercel installs into your repo as a GitHub App that watches for pushes and creates deployments in response. It doesn’t push your code anywhere; your git history is the source of truth, and Vercel reacts to it. The deploy isn’t a step you perform after the push, it is the push.

Sort each git action into the deployment environment it triggers. Watch the trap: not every push lands in production. Drag each item into the bucket it belongs to, then press Check.

Production The alias the custom domain points at
Preview A throwaway *.vercel.app build per PR branch
Development Your machine, reading .env.local
Merging a PR into main
Pushing a hotfix directly to main
Pushing a commit to an open PR branch
A second push to the same PR branch
Opening a new pull request
Running vercel dev on your laptop
Reading .env.local during pnpm dev

Deployments are immutable; production is just an alias

Section titled “Deployments are immutable; production is just an alias”

Every push produces a brand-new deployment: its own build, its own packaged output, its own permanent URL of the shape <hash>-<project>.vercel.app. Vercel never reaches back into an existing deployment to change it. That’s what makes a deployment immutable : every build is a frozen, finished thing that lives until you explicitly delete it, which is also the one action that removes it as a rollback target.

So what is “production”? Not a deployment at all: a pointer. Your custom domain (app.example.com) and the project’s main *.vercel.app URL are an alias , a label aimed at exactly one deployment at a time. “Deploying to production” doesn’t make a build become production; it re-aims that pointer at a new deployment. The swap is atomic and instant: no rebuild, no copy, just a name now pointing at a different frozen value.

This should feel familiar. In “Values, references, and copies,” you learned that a JavaScript variable isn’t a box holding a value; it’s a name bound to a value, and reassigning it just re-points the name. Production is the same idea on a domain:

  • app.example.com is a let binding.
  • Each deployment is an immutable value, like a frozen object you can’t mutate.
  • “Deploy” is reassignment: app.example.com = <new deployment>.
  • Reassigning doesn’t destroy the old values; the name just no longer points at them.
production alias app.example.com
a1b2 production
fix invoice total
a1b2-app.vercel.app
9f0e
add export button
9f0e-app.vercel.app
Two deployments exist. The production domain points at a1b2, the latest push to main.
production alias app.example.com
c3d4 built · not aliased
new pricing page
c3d4-app.vercel.app
a1b2 production
fix invoice total
a1b2-app.vercel.app
9f0e
add export button
9f0e-app.vercel.app
A new push to main builds c3d4. It's live on its own URL, but the domain hasn't moved.
production alias app.example.com
c3d4 production
new pricing page
c3d4-app.vercel.app
a1b2
fix invoice total
a1b2-app.vercel.app
9f0e
add export button
9f0e-app.vercel.app
Vercel re-aliases the domain to c3d4. The swap is atomic — no rebuild. a1b2 and the rest stay live at their URLs, ready to roll back to.

Two consequences fall straight out. First, rollback is free: every previous deployment is still alive at its own URL, so recovering means re-aiming the pointer at a known-good one, no rebuild. You’ll build that flow later, but you already know why it works: instant rollback falls out of immutability plus an alias.

Second, a <hash>-<project>.vercel.app deployment URL is not “the app.” Each one is pinned to a single deployment forever; only the production alias moves. Send a colleague that URL as “here’s our app” and you’ve handed them a frozen snapshot that stays put long after production has moved on. Share the production domain, never a deployment URL.

A push produces an immutable deployment, and going live is a pointer swap. What runs between them? You rarely need to look inside, but you do need the order and one relationship that trips most people up.

In order, when you push to main, Vercel:

  1. Fetches the pushed commit.
  2. Runs the install, pnpm install, inside a fresh Linux container.
  3. Runs the build, pnpm build / next build, in that container, with environment variables scoped to the target environment (production vars for a main push, preview vars for a PR push).
  4. Packages the output into Functions artifacts : bundled server functions plus static assets.
  5. Deploys it globally, live on its own deployment URL.
  6. Re-aliases the production domain to the new deployment, the pointer swap from the previous section.

Notice where the alias swap sits: dead last, after the deployment is already live on its own URL. That gap, a working deployment that production doesn’t yet point at, is exactly what rollback exploits.

Now the relationship to get crisp, because the wrong version of it leads to real trouble. You might assume your CI gate from the last chapter protects production, that a push to main can’t ship until the four jobs go green. It doesn’t. The Vercel deploy and the GitHub Actions CI run in parallel; the deploy does not wait for CI, and can re-alias production before, or even while, CI is still running.

So what was your CI gate protecting? Merges. The branch ruleset blocks a broken PR from merging into main, which keeps main healthy, which keeps production healthy, one step upstream of the deploy. To make the deploy itself wait on CI, the tool is Vercel Deployment Checks , which hold a freshly built deployment un-aliased until your external checks pass; we wire it up later. The distinction to carry away: your ruleset gates merges to main, not the production alias.

This pipeline also forces you to understand when an environment variable is read, because the mismatch is the most common “I changed the env var and nothing happened” bug.

  • Some variables are read at build time and baked into the artifact: anything prefixed NEXT_PUBLIC_* (inlined directly into the client bundles), the env.ts validator, anything used during static generation.
  • Others are read at runtime from the running environment: server-only secrets that your server actions and route handlers read when a request comes in.

This is the immutability rule again: the artifact is frozen, so anything baked into it at build time is frozen too. Change a NEXT_PUBLIC_* value in the dashboard and the deployments already built still carry the old value, soldered into their bundles; only a rebuild (a new deployment) picks it up. A runtime secret, by contrast, is re-read on the next request, so it changes without a rebuild. So when you change a public variable and the live site doesn’t budge, the fix is to ship a new build.

A deploy ships code and nothing else, and the worst incidents live in the gap between “code shipped” and “system actually consistent.” Three pieces of that gap are worth naming:

  • No database migration runs. Drizzle migrations are a separate, deliberate step, the subject of the next chapter. Shipping code that expects a new column does not create the column; the deploy that references it just starts failing against a schema that doesn’t have it yet.
  • No external CDN gets purged. A cache you put in front of Vercel (a Cloudflare layer, covered later in this chapter) has its own lifetime. A new deploy doesn’t reach through and clear it.
  • No secrets rotate. A deploy never touches your credentials. Rotating a leaked key is its own action, on its own schedule.

The principle tying all three together: the deploy ships code, and everything stateful is your responsibility. Vercel will happily ship code that assumes a database, cache, or secret is in a state it’s not. Knowing the gap before you ship is the difference between a planned migration and a 2 a.m. page.

Git drives the normal path: you push, Vercel deploys, you never open a terminal. The CLI is for the off-path moments, when you step outside the git flow on purpose. What matters is knowing when you’d reach for each command.

Terminal window
vercel
vercel --prod
vercel env pull .env.local
vercel logs <url>
vercel inspect <url>
vercel promote <url>
  • vercel deploys the current directory as a preview by hand, a quick throwaway build without opening a PR.
  • vercel --prod deploys to production straight from your laptop, bypassing PR review and CI. Reach for it almost never; it’s here so you recognize it as the wrong tool in normal work.
  • vercel env pull .env.local syncs the development-scoped variables down to your local .env.local, the first command you run after cloning a repo. The next lesson sets it up properly.
  • vercel logs <url> streams a deployment’s logs, your first move when one is misbehaving.
  • vercel inspect <url> shows a deployment’s metadata and build output, for digging into why a build came out the way it did.
  • vercel promote <url> re-aliases production to an existing deployment, the scriptable pointer swap that instant rollback is built on. The rollback lesson uses it in earnest.

It all collapses to one rule: reach for the CLI during an emergency rollback, for local dev with production-shaped variables, or to inspect a failed build, and otherwise let git drive.

Cloudflare Pages, Netlify, AWS Amplify, and a self-hosted Node server all deploy Next.js and can host a real product. The course commits to Vercel because Next.js and Vercel are built by the same team, so new framework features ship on Vercel first and work there without extra configuration.

The next lesson puts your first production URL on the web.