You run pnpm dev and open localhost:3000. Your teammate opens a preview link on a pull request.
A shopper opens shop.com. All three see the same app, but each one runs in a different place.
Each place is an . Most teams have three: local, staging and production. In this lesson you learn what lives in each, and why the team keeps them apart.
Three places, one app
Each environment has a job:
- Local is for building.
- Staging is for checking.
- Production is for real users.
┌────────────────────────────────────────────────────┐ │ │ │ local staging production │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ your │ ──▶ │ team │ ──▶ │ real │ │ │ │ laptop │ │ tests │ │ users │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ ┌────┴─────┐ ┌────┴─────┐ ┌────┴─────┐ │ │ │ fake │ │ test │ │ real │ │ │ │ data │ │ data │ │ data │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └────────────────────────────────────────────────────┘
- Local is your laptop. You can break it a hundred times a day, and nobody else notices.
- Staging is a shared copy that looks like production. The team and the robots test there before users see a change. Some teams call it "pre-production", "QA" or "test".
- Production is the live app. Real users, real money, real data.
What lives in each
The code is the same in every environment. The things around the code are not. Here is a typical setup for a Next.js shop:
| Local | Staging | Production | |
|---|---|---|---|
| Who uses it | You | Your team | Real users |
| Address | localhost:3000 | staging.shop.com | shop.com |
| Database | Fake seed data | Test data | Real customer data |
| Stripe keys | Test keys | Test keys | Live keys |
| Emails | Caught, not sent | Sent to the team | Sent to customers |
| When it breaks | Only you notice | The team notices | Users notice |
Look at the Stripe row. Stripe gives every account two sets of keys:
- A test key accepts fake card numbers and moves no money.
- A live key charges real cards.
A test key in production means no orders get paid. A live key on your laptop means a test charges a real card. The environment decides which key the app gets.
One build, different settings
Last lesson had a rule: build once, and send the same artifact everywhere. So how does one artifact know which database to use?
The answer is .
The app does not contain the database address. It reads the address when it starts. Each environment gives it a different value.
┌─────────────────────────────────────────────┐ │ │ │ ┌───────────────────┐ │ │ │ build a1b2c3d │ │ │ │ same in every env │ │ │ └─────────┬─────────┘ │ │ ┌─────────────┼─────────────┐ │ │ ▼ ▼ ▼ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ local │ │ staging │ │ prod │ │ │ ├─────────┤ ├─────────┤ ├─────────┤ │ │ │ db dev │ │ db stg │ │ db prd │ │ │ │ pay fake│ │ pay test│ │ pay live│ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ └─────────────────────────────────────────────┘
The most common way to pass settings is an . In a Next.js app you already use them:
# Read by next dev on your laptop. Never committed.
DATABASE_URL=postgres://localhost:5432/shop
STRIPE_SECRET_KEY=sk_test_51Hx...DATABASE_URLtells the app where its database is. On your laptop, that is a database on your own machine.sk_test_at the start of the key marks a Stripe test key. A live key starts withsk_live_.- On Vercel, you do not use this file. You set the same names in the project settings, with one value for Preview and one for Production.
Docker in Action shows the same idea with containers. A greetings service ships as one image. At deploy time, a variable called DEPLOY_ENV picks a settings file for dev, stage or prod. The image never changes.
The real pipeline on Vercel
Here is how a Next.js team on Vercel moves one change through its environments:
preview
every push
- Previewits own URL
- Preview varsTest databasefake orders
production
approved
- Merge to mainafter review
- Productionshop.com
- Production varsReal databasereal orders
Vercel gives every project three environments: Local, Preview and Production. Paid plans can add named ones, such as staging. Each environment has its own set of variables.
Keep them alike, keep them apart
Two rules pull in different directions, and good teams follow both.
Keep them alike. Staging only helps if it acts like production. If production runs PostgreSQL 16 and your laptop runs SQLite, a query can pass locally and fail live. The Twelve-Factor App guide calls this the gap between dev and prod. It asks teams to use the same kind and version of each service in every environment. Docker, later in this course, makes that much easier.
Keep them apart. Staging must never touch production data by mistake. Separate databases, separate keys and separate access stop a test from hurting real users.
Many teams now make production look different on purpose: a red banner in the admin page, a red shell prompt on production servers, and fewer people with access. The goal is simple. You should always know where you are.
- 01
Most teams run the same app in three environments: local for building, staging for checking, production for real users.
- 02
The code is the same everywhere. Configuration, often environment variables, tells each copy which database and keys to use.
- 03
On Vercel, every pull request gets a preview deployment, and Preview and Production each get their own variables.
- 04
Keep environments alike so tests mean something, and apart so a test never touches real data.
Read more
- Environments from Vercel. Local, Preview, Production and custom staging.
- Environment variables in Next.js. The
.envfiles, load order and theNEXT_PUBLIC_rule. - The Twelve-Factor App: dev/prod parity. A short page on keeping environments alike.
- Docker in Action, 2nd edition, chapter 12: read 12.2 on one image with settings per environment.
- GitLab's database outage postmortem, the story above.
Next, you get a map of the DevOps toolbox: every tool you will hear about, sorted by its job.