Web development basics · Lesson 8 of 11 · about 35 minutes

Deploying to Vercel

The bakery's site goes on the internet. One script creates a throwaway Vercel project and puts it through a release day: a preview, five production deployments, changed settings, a bad release, a rollback and a build that fails. The surprises were the useful part: a fix that didn't go live after a rollback, settings that changed nothing until the next deploy, and a first deploy that failed after a successful build.

By the end you'll know

  • What a deployment is on Vercel, and the difference between the production domain, a deployment's own URL and a preview
  • How environment variables work per environment, and why changing one does nothing until you deploy again
  • Who can open a preview, and how to get past the login when you need to
  • What HTTPS and caching look like on a real host
  • How to roll back a bad release in seconds, and the step after a rollback that's easy to miss

Before you start

  • You need: Lessons 4 and 6: the bakery's Next.js app, environment variables and the revalidating menu page. Node.js 20 or later, and a free Vercel account.
  • Versions: Vercel CLI 65.12.1 and Next.js 16.4.5 on Vercel's free Hobby plan, November 2026.
  • The example: Juniper & Rye, the made-up bakery. The script creates a Vercel project called juniper-rye-lesson-8 in your account and a cleanup script deletes it. Every run and line of output below is real. The script prints labels like "Production domain" instead of addresses, because Vercel's addresses include your account name.

The short version

  • A deployment is a frozen copy: your code, built once, with the environment variables of that moment. It never changes. Releasing means pointing the production domain at a different one.
  • Changing a variable does nothing until you deploy again. Twenty seconds after a change, the live site still showed the old values. After a new deployment, both changed.
  • Only the production domain is public by default. Previews, and each deployment's own URL, redirected to Vercel's login.
  • A rollback took 2 seconds, and then the next deploy didn't go live. After a rollback, Vercel stops releasing new production deployments until you promote one.
  • A failed build never reaches visitors. The broken release stopped at the build, and the site stayed on the last good one.

What Vercel does with your code

So far the bakery has run on your machine with npm run build and npm start. Vercel, the company behind Next.js, runs those steps for you on its servers and serves the result from data centres around the world.

Three words come up constantly:

  • A deployment is one build of your code, with its own URL, kept as it is. Five deploys means five deployments, all still there.
  • The production domain is the address customers use. It points at one production deployment at a time.
  • A preview is a deployment that isn't production: for checking a change before customers see it. Previews have their own environment variables.

The practice project

The practice folder, lesson-08, has a small version of the bakery site in site/. Its home page shows four things, read on every request:

JS
<dt>Release</dt><dd id="release">{RELEASE}</dd>
<dt>Environment</dt><dd id="env">{process.env.VERCEL_ENV ?? "local"}</dd>
<dt>Message (server-only variable)</dt><dd id="message">{process.env.BAKERY_MESSAGE ?? "(not set)"}</dd>
<dt>Banner, as the server saw it</dt><dd id="banner-server">{process.env.NEXT_PUBLIC_BANNER ?? "(not set)"}</dd>
  • RELEASE comes from site/release.js, which the script changes before each deploy: v1, v2 and so on. Release v3 throws an error, on purpose.
  • VERCEL_ENV is set by Vercel: production, preview or development.
  • BAKERY_MESSAGE is a server-only variable, and NEXT_PUBLIC_BANNER a public one (Lesson 6).

It also has the menu page from Lesson 6, rebuilt at most every 10 seconds. check.mjs fetches a page and prints what it found; run-all.sh does everything else.

(Your machine)

BASH
cd lesson-08
chmod +x run-all.sh cleanup.sh
npx --yes vercel@65.12.1 login
./run-all.sh
  • chmod +x makes the scripts runnable.
  • vercel login signs the command line tool (CLI) in to your Vercel account, through your browser.
  • ./run-all.sh runs every step below, a few minutes in all, and saves what it found in out/. The CLI's full output, which includes your addresses, goes to raw/.

The sections below show the commands the script runs, and what came back.

Creating the project

BASH
npx --yes vercel@65.12.1 project add juniper-rye-lesson-8
cd site
npx --yes vercel@65.12.1 link --yes --project juniper-rye-lesson-8

project add creates an empty project in your Vercel account. link connects the site folder to it, by writing a .vercel folder (which is in .gitignore).

The first deploy failed

The first time this ran, the build worked and the deploy failed anyway. The end of Vercel's build log:

TEXT
Error: No Output Directory named "public" found after the Build completed. Configure the Output Directory in your Project Settings. Alternatively, configure vercel.json#outputDirectory.

The project had been created empty from the command line, so it had no framework preset: Vercel treated it as a plain website and looked for a public folder to serve. Vercel normally detects the framework when a project is set up from your code, from the dashboard or a first vercel deploy; an empty project skipped that. The fix that keeps the setting in the code is site/vercel.json:

JSON
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "framework": "nextjs"
}

The next build's log said Detected Next.js version: 16.4.5 and worked.

Environment variables, per environment

Each variable is set separately for production and for previews:

BASH
vercel env add BAKERY_MESSAGE --environment production --value "Rye week starts Tuesday" --yes
vercel env add BAKERY_MESSAGE --environment preview --value "PREVIEW: testing the new menu" --yes
vercel env add NEXT_PUBLIC_BANNER --environment production --value "Order by 8pm for next-day pickup" --yes
vercel env add NEXT_PUBLIC_BANNER --environment preview --value "Preview build, not the real site" --yes

(The script runs each as npx --yes vercel@65.12.1 env add ....) Vercel answered the first with:

TEXT
  Stored as Secret by default. This value is hidden in the dashboard and unavailable to pulls; use `--type config` for values you need to read later.

and each NEXT_PUBLIC_ one with:

TEXT
! `NEXT_PUBLIC_` exposes this value to anyone visiting your site

Both are good defaults. A secret value can't be read back, even by you: not in the dashboard, and not by vercel env pull, which downloads variables to a file. Keep the real value somewhere safe, like a password manager. And the warning is Lesson 6's rule, from Vercel: never put a key in a NEXT_PUBLIC_ variable.

Previews: who can see them

BASH
vercel deploy --yes

With no --prod, a deploy is a preview. The script then fetched it without logging in, and again with vercel curl, a CLI command that adds a pass to get through Vercel's login:

TEXT
Preview URL, no login: HTTP 302 -> vercel.com/sso-api
Preview URL through vercel curl, which adds a bypass:
  release v1, environment preview
  message: PREVIEW: testing the new menu
  banner:  Preview build, not the real site
  • A stranger was sent to Vercel's login. New projects come with Deployment Protection set to Standard Protection: every URL except the production domain needs a Vercel login with access to the project.
  • The preview used the preview values. Same code as production would get, different settings. Previews should have their own keys too, like a test API key, so a preview can't touch real data.

Production

BASH
vercel deploy --prod --yes
TEXT
Production domain: HTTP 200
  release v1, environment production
  message: Rye week starts Tuesday
  banner:  Order by 8pm for next-day pickup
v1's own deployment URL: HTTP 302 -> vercel.com/sso-api

The production domain is public; the same deployment at its own URL is not. Every deployment gets a unique address. Standard Protection treats those as private, so old releases can't be opened by anyone who finds their address.

HTTPS, without asking

The script asked the production domain over plain HTTP, then over HTTPS:

TEXT
Production domain over plain HTTP: HTTP 308 -> https://(the same site)/
  over HTTPS: strict-transport-security: max-age=63072000; includeSubDomains; preload
  • 308 is a permanent redirect to the HTTPS address.
  • strict-transport-security (HSTS) tells the browser to use HTTPS for this site for the next two years (63,072,000 seconds), without even trying HTTP first.

This is where Lesson 7's secure=false login cookie stops being fine. On an HTTPS site, cookies that matter should be marked Secure, so the browser only ever sends them encrypted. Check the flag on your real site, not on localhost.

Changing a variable

Next, the script changed both production values, the way you would in the dashboard, and waited 20 seconds:

BASH
vercel env update BAKERY_MESSAGE --environment production --value "Rye week moved to Wednesday" --yes
vercel env update NEXT_PUBLIC_BANNER --environment production --value "Order by 6pm for next-day pickup" --yes
TEXT
Production domain, 20 s later: HTTP 200
  release v1, environment production
  message: Rye week starts Tuesday
  banner:  Order by 8pm for next-day pickup

Nothing changed, even though the page reads both values on every request. Then it deployed release v2:

TEXT
Production domain: HTTP 200
  release v2, environment production
  message: Rye week moved to Wednesday
  banner:  Order by 6pm for next-day pickup

A deployment takes the variables as they are when it's built, and keeps them. Changing a setting changes what the next deployment will get, not the running one. Two reasons, one for each kind:

  • NEXT_PUBLIC_ values are written into the built JavaScript. Even the server's copy: in a local test, a build with one banner value kept showing it when the server was started with another.
  • Server-only values are fixed per deployment by Vercel, so a deployment behaves the same every time it runs, including after a rollback.

So after changing a variable, deploy again. That's also why rotating a leaked key is "change it, then redeploy".

Caching on Vercel

The menu page from Lesson 6 rebuilds at most every 10 seconds. On Vercel, the x-vercel-cache header says where each answer came from:

TEXT
  0 s  HTTP 200  x-vercel-cache: PRERENDER built at 04:52:35
  2 s  HTTP 200  x-vercel-cache: HIT    built at 04:52:41
 12 s  HTTP 200  x-vercel-cache: STALE  built at 04:52:41
 14 s  HTTP 200  x-vercel-cache: HIT    built at 04:52:53
  • PRERENDER: the copy made during the build. It was already more than 10 seconds old, so this request also set off a rebuild, which is why the next answer was a newer copy.
  • HIT: a saved copy, still within its 10 seconds. Served straight from Vercel's cache, without running your code.
  • STALE: older than 10 seconds. The visitor got it anyway, and a rebuild started in the background. The request two seconds later got the new copy.

That's Lesson 6's stale-while-revalidate, now on a real host, where Vercel's cache sits in front of your code.

When a release goes wrong

A bad release, and a rollback

Release v3 has a bug that throws on the home page. It built fine, so it went live:

TEXT
Production domain, v3 live: HTTP 500

Every customer now gets an error. The fastest fix isn't a fix; it's going back:

BASH
vercel rollback <v2's deployment URL> --yes
TEXT
Production domain, after rollback: HTTP 200
  release v2, environment production
  message: Rye week moved to Wednesday
  banner:  Order by 6pm for next-day pickup

The CLI reported the rollback done in 2 seconds. Nothing was rebuilt: v2 still existed, exactly as it was, and the production domain was pointed back at it. On the Hobby plan you can roll back to the previous production deployment; paid plans can go further back.

The fix that didn't go live

The bug is fixed in v4, and deployed to production as usual:

TEXT
Production domain, after deploying v4: HTTP 200
  release v2, environment production
  message: Rye week moved to Wednesday
  banner:  Order by 6pm for next-day pickup
v4's own URL, no login: HTTP 302 -> vercel.com/sso-api

Still v2. The deploy succeeded, but after a rollback Vercel stops pointing the production domain at new production deployments. It's deliberate: if the bad release came from an automatic deploy, you don't want the next push to undo your rollback before you've checked it. But it's easy to deploy the fix, see "Ready", and walk away with the old version still live.

The next step is to promote the fix:

BASH
vercel promote <v4's deployment URL> --yes
TEXT
Production domain, after promote: HTTP 200
  release v4, environment production
  message: Rye week moved to Wednesday
  banner:  Order by 6pm for next-day pickup

Promoting points the domain at v4 and turns automatic releases back on. The dashboard does the same with its Undo Rollback button.

A release that doesn't build

Last, release v5 has a syntax error, so the build fails. A follow-up script, extra.sh, read the saved results of all five production deploys:

TEXT
prod-v1: READY, aliased to the production domain: yes
prod-v2: READY, aliased to the production domain: yes
prod-v3: READY, aliased to the production domain: yes
prod-v4: READY, aliased to the production domain: no
prod-v5: ERROR (Command "npm run build" exited with 1), aliased to the production domain: no

"Aliased" is Vercel's word for pointing a domain at a deployment. v4 confirms what you saw: ready, not made live. v5 never got that far, and the site carried on:

TEXT
Production domain: HTTP 200
  release v4, environment production
  message: Rye week moved to Wednesday
  banner:  Order by 6pm for next-day pickup

A build that fails can't reach customers. That's the strongest reason to make mistakes fail the build: Lesson 6's server-only, a type check, a test step. Every error caught there is an error visitors never see. v3 is the other kind: it built, then failed for visitors, which only a check after deploying would catch.

Five production deployments, and which one customers got after each step. Select the diagram to enlarge it.

Cleaning up

(Your machine)

BASH
./cleanup.sh

It runs vercel project rm juniper-rye-lesson-8 --confirm-name juniper-rye-lesson-8, which deletes the project and all its deployments, then removes the local .vercel link. Vercel asks you to type the name to confirm deletes like this, so they can't happen by accident.

In real systems

  • Connect the Git repository instead of deploying from your machine. Every push to a branch then makes a preview, every pull request gets its preview's link, and merging to the main branch deploys production. The commands in this lesson are what that automation does for you.
  • Use your own domain. Add it in the project's Domains settings and point your DNS (domain name system) records at Vercel; HTTPS certificates are issued and renewed for you. The production domain then works just like the .vercel.app one here.
  • Check production after every release. A small script like check.mjs that fetches the home page and one key page, run after each deploy, would have caught v3 within seconds.
  • Know your rollback before you need it. Who can run it, how far back your plan allows, and that the next step is a promote.
  • Rollbacks don't roll back data. If a release changed the database (Lesson 7's migrations), going back to the old code doesn't undo that. Make database changes that old and new code can both live with.
  • Give previews their own credentials: a test Supabase project, test API keys. A preview with production keys can change production data.

Spot the problem

Work out what's wrong before you read the explanation.

"We rotated the key. Why is the old one still working?" A shop's payment provider key leaks in a screenshot. The developer creates a new key, pastes it into the hosting dashboard's environment variables, saves, and deletes the old key at the provider a few minutes later. Within minutes, checkout fails for every customer with "invalid API key". What's wrong? The running deployment still had the old key. Saving a variable only affects the next deployment; deleting the old key at the provider broke the one customers were using. The order that works: add the new key, redeploy, check checkout works on the new deployment, then delete the old key. Then check: does the live deployment's build date come after the variable change?

Keep this

ActionCommandWhat happened
Previewvercel deployNew deployment with preview variables; behind Vercel's login
Releasevercel deploy --prodNew deployment; production domain points at it, if the build works
Change a variablevercel env updateNothing, until the next deployment
Roll backvercel rollbackDomain points at an earlier deployment in seconds; new deploys stop going live
Promotevercel promoteDomain points at the chosen deployment; new deploys go live again
AlwaysSet the framework; separate preview credentials; check the site after each release

Where you are now

You can deploy a Next.js site to Vercel from the command line, give production and previews their own settings, and explain why a changed setting needs a new deployment. You've seen what's public and what isn't, what HTTPS and caching look like on a real host, and how to recover from a bad release, including the promote that a rollback leaves for you. The next lesson is about speed: measuring how fast the bakery's pages load, and what makes them slow.

Check yourself

  1. You changed BAKERY_MESSAGE in the project settings. When did the live site show the new value?

  2. A stranger opened the preview's URL. What happened?

  3. Why did the very first deploy fail, after a successful build?

  4. After rolling back to v2, you deployed the fix, v4, to production. What did customers see?

  5. Release v5 failed to build. What did the production domain serve?

  6. The menu answered with x-vercel-cache: STALE. What does that mean?

Key terms

  • Deployment: one build of the site, with its own URL and the variables of that moment. It never changes.
  • Production domain: the address customers use, pointed at one production deployment at a time.
  • Preview deployment: a deployment that isn't production, with its own environment variables.
  • Framework preset: the project setting that tells Vercel how to build and serve the code.
  • Deployment Protection: Vercel's login in front of deployments; Standard Protection covers everything except the production domain.
  • Alias: Vercel's word for pointing a domain at a deployment.
  • Rollback and promote: pointing the production domain at an earlier deployment, and at a chosen one (which also restores automatic releases).
  • HSTS (HTTP Strict Transport Security): a header telling browsers to use only HTTPS for a site.
  • x-vercel-cache: a response header saying whether Vercel answered from its cache (HIT), with an old copy while rebuilding (STALE) or with the copy made at build (PRERENDER).

Go deeper