Skip to content
VerifiedUpdated 2026-06-13
On this page

Vercel is the path of least resistance for Next.js 16. You connect a GitHub repo once, and every push becomes a deploy: pull requests get isolated preview URLs, and the default branch becomes production. No Dockerfile, no next start process to babysit, no server to patch. Your job is to make the build deterministic and keep secrets where the browser cannot reach them.

This page takes you from a local repo to a live, public URL, with a pre-ship gate that stops broken builds before they leave your machine.

How a Vercel deploy actually works

Vercel does not deploy your local folder. It deploys a Git commit. When you connect a repo, Vercel installs a GitHub app that listens for pushes. On every push it checks out that exact commit, runs npm install then next build in a clean container, and serves the output.

Two consequences fall out of this, and both bite people who skip them:

  1. If a file is in .gitignore (and .env.local always is), Vercel never sees it. Environment variables must be set in Vercel, not shipped in the repo.
  2. The build runs in a clean container, not your machine. "Works on my machine" means nothing. If your lockfile is stale or a type error is hiding, the Vercel build fails where your local dev server happily ignored it.

Push your app to GitHub

Vercel deploys from Git, so the first move is getting your code onto GitHub. If your project is not yet a repo, initialize it.

Terminal
git init
git add .
git commit -m "Initial Next.js 16 app"

Create an empty repository on GitHub (no README, no .gitignore, no license, you already have those), then point your local repo at it and push.

Terminal
git remote add origin https://github.com/your-username/your-app.git
git branch -M main
git push -u origin main

Before you push, confirm your secrets are not riding along. create-next-app adds every .env* file to .gitignore for you, but verify it.

Terminal
git check-ignore .env.local
# prints ".env.local" if it is correctly ignored; prints nothing if it is NOT

Import the repo into Vercel

In the Vercel dashboard, click Add New, Project, then Import next to your GitHub repository. If you do not see it, click Adjust GitHub App Permissions and grant Vercel access to that repo.

Vercel detects Next.js automatically and fills in the build settings. You do not need to touch them. For reference, the defaults are:

  • Framework Preset: Next.js
  • Build Command: next build
  • Install Command: npm install
  • Output Directory: handled by the Next.js adapter, leave it blank

Pin the Node.js version so local and remote match. In Settings, General, Node.js Version, choose 20.x (or 22.x), matching the version you develop with. Do not click Deploy yet. Set your environment variables first (next step), or the first build can succeed while the running app crashes on a missing key.

Set production environment variables

.env.local is your local-only file and it never leaves your machine. Its production counterpart lives in Settings, Environment Variables in the Vercel project. Every key your app reads from process.env at build time or runtime must be defined there, or the build inlines undefined.

Take a set of keys like this from your local file:

.env.local
DATABASE_URL=postgres://localhost:5432/dev
STRIPE_SECRET_KEY=sk_test_local_key
NEXT_PUBLIC_SITE_URL=http://localhost:3000

In Vercel, add each one with its production value. For each variable you pick which environments it applies to: Production, Preview, and Development. A live database URL belongs in Production; a staging URL belongs in Preview. Keep them separate so a preview deploy can never write to production data.

The code that reads them does not change between local and Vercel. Server-only secrets stay on the server:

app/page.tsx
import { connection } from "next/server";
 
export default async function Page() {
  // Opt into dynamic rendering so the value is read at runtime, not frozen at build.
  await connection();
  const dbUrl = process.env.DATABASE_URL; // server-only, never sent to the browser
  // ...use dbUrl to fetch data
  return <main>Connected</main>;
}

Anything the browser needs must be prefixed NEXT_PUBLIC_. That prefix tells Next.js to inline the value into the client bundle at build time.

Prefix a variable with NEXT_PUBLIC_ only when the browser genuinely needs it. That prefix bakes the value into the JavaScript shipped to every visitor.

Never put a secret (an API secret key, a database password, a private token) behind NEXT_PUBLIC_. It gets inlined into the client bundle and is then readable by anyone who opens devtools.

Avoid

.env.local
# This secret ends up in the browser bundle. Anyone can read it.
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_realmoney

Prefer

.env.local
# Secret stays server-side. Only the publishable key is public.
STRIPE_SECRET_KEY=sk_live_realmoney
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_safe

Trigger the first deploy and learn preview vs production

With variables in place, click Deploy. Vercel checks out your commit, runs the install and build, and gives you a production URL on a *.vercel.app domain. Watch the build logs stream live; if next build fails, the log line tells you exactly which file and which error.

Now you have the two-track workflow that makes Vercel worth using:

  • Production: pushing to main rebuilds and replaces your live site at its production URL.
  • Preview: open a pull request, or push to any non-default branch, and Vercel builds a throwaway deploy at a unique URL. Vercel comments that URL on the PR. It uses your Preview environment variables, so you can click through real behavior before merging.

Exercise it once so the loop is muscle memory:

Terminal
git checkout -b feature/new-landing
# make a visible change to app/page.tsx
git commit -am "New landing hero"
git push -u origin feature/new-landing

Open the pull request on GitHub. Within a minute Vercel posts a preview link. Review the preview, and when you merge to main, that same change promotes to production automatically.

Review every change on its preview URL before merging to main. The preview is built from the exact commit that will become production, so what you see is what ships.

Gate every deploy behind a green local build

The Vercel build container runs next build with no mercy. Catch failures locally first, because a red build on main means your production site is stuck on the last good deploy while you scramble. Run the same three checks Vercel effectively depends on, in order, before you push.

Note one Next.js 16 change: next build no longer runs the linter for you. You run lint as its own step. Add scripts so the gate is one command.

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "typecheck": "tsc --noEmit",
    "ship": "npm run typecheck && npm run lint && npm run build"
  }
}

Run the gate before every push:

Terminal
npm run ship

What each step buys you:

  • tsc --noEmit: type errors that the dev server tolerates but a strict build rejects. params is a Promise in Next.js 16, so a route that forgets to await it is a type error caught here, not in production.
  • eslint: the lint pass next build used to run for you. Catches unused imports, bad hook usage, and accessibility issues.
  • next build: the real production compile. If this is green locally with the same Node version, Vercel will almost certainly be green too.
app/posts/[slug]/page.tsx
// Next.js 16: params is a Promise and must be awaited.
// Forgetting the await is a type error that `npm run typecheck` catches.
export default async function Post({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <article>Post: {slug}</article>;
}

Map your production domain (optional)

A *.vercel.app URL is real and public, good enough to share. When you are ready for a custom domain, go to Settings, Domains, add your domain, and follow the DNS records Vercel shows you. Vercel provisions and renews TLS certificates automatically, so HTTPS is on by default with nothing to configure. That is the secure-by-default baseline you should expect and never downgrade from.

Sources

  • Next.js docs01-app/01-getting-started/01-installation.md
  • Next.js docs01-app/02-guides/environment-variables.md