DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Deployment and CI/CD for Next.js 15 The Setup I Use for Every Project

For a while my "deployment process" was pushing to main and hoping Vercel's build did not fail. It usually worked, until a project grew past a single developer and untested code started reaching production because nothing ran before the deploy actually happened.

Here is the setup I use now, simple enough to not slow a solo project down, solid enough to actually catch problems before they ship.


1. Vercel Handles the Deploy, GitHub Actions Handles the Checks

This is the part people overcomplicate. Vercel already builds and deploys automatically on every push, no extra configuration needed for that part. What is usually missing is anything running before that deploy to catch a broken build, a failing test, or a type error before it reaches production.

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Type check
        run: npx tsc --noEmit

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm run test -- --run

      - name: Build
        run: npm run build
Enter fullscreen mode Exit fullscreen mode

This runs on every pull request and every push to main. A broken build or failing test shows up as a red X directly on the PR, before Vercel ever tries to deploy it, and before a teammate merges something that looks fine locally but breaks in CI.


2. Why the Build Step Matters Even Though Vercel Also Builds

Running npm run build in GitHub Actions looks redundant since Vercel builds the project too. The difference is timing. Vercel's build happens after the merge, when the broken code is already on main. Catching the same failure in CI on the pull request means it never reaches main at all.


3. Branch Protection to Actually Enforce This

A CI check that runs but does not block anything is just a suggestion. In your GitHub repo settings, under branch protection rules for main:

  • Require status checks to pass before merging
  • Select the checks job from the workflow above as required
  • Optionally require at least one approving review

Without this, CI can fail loudly on a pull request and someone can still click merge anyway. Branch protection is what turns the checks from advisory into an actual gate.


4. Preview Deployments for Every Pull Request

Vercel does this automatically once your GitHub repo is connected, every pull request gets its own live preview URL, posted directly as a comment on the PR. This alone catches a large category of bugs that only show up when something is actually deployed, not just running locally.

For a client project, sending the preview URL directly instead of a screenshot or a description means the client can click around a real, working version of the change before it goes live.


5. Environment Variables Across Environments

A common mistake is using the same environment variables for local development, preview deployments, and production, especially with things like a Stripe key or a database URL that should genuinely differ between them.

In Vercel's project settings, environment variables can be scoped separately:

MONGODB_URI           Production only (real database)
MONGODB_URI           Preview (staging database, separate from production data)
MONGODB_URI           Development (local .env.local, not in Vercel at all)

STRIPE_SECRET_KEY     Production (live key)
STRIPE_SECRET_KEY     Preview (test key)
Enter fullscreen mode Exit fullscreen mode

This matters most for Stripe and the database. Without separate values, a preview deployment testing a checkout flow could accidentally hit your real Stripe account or write test data into your production database.


6. Running Database Migrations Safely

For projects with schema changes that need to run before new code goes live, a manual migration script triggered separately from the deploy is safer than running it automatically on every build:

# .github/workflows/migrate.yml
name: Run Migration

on:
  workflow_dispatch: # manually triggered, not automatic

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run migrate
        env:
          MONGODB_URI: ${{ secrets.PRODUCTION_MONGODB_URI }}
Enter fullscreen mode Exit fullscreen mode

workflow_dispatch means this only runs when manually triggered from the GitHub Actions tab, not automatically on every push. Schema changes against a production database are exactly the kind of thing that benefits from a deliberate, manual trigger rather than happening silently as part of every deploy.


7. Rolling Back a Bad Deploy

Even with checks in place, something can still slip through, a runtime bug that only shows up under real production data or traffic patterns CI never sees. Vercel keeps every previous deployment available, and rolling back is a matter of promoting the last known-good deployment back to production from the dashboard, no new build required, no waiting on a fix to be written first.

Worth doing this once on a low-stakes project just to know the process before you actually need it under pressure.


8. Keeping the CI Workflow Fast

A CI check that takes ten minutes gets ignored or worked around eventually. A few things that keep it quick:

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm' # caches node_modules based on package-lock.json
Enter fullscreen mode Exit fullscreen mode

Caching node_modules based on the lockfile hash means npm ci reuses the cache instead of reinstalling every dependency from scratch on every single run, which is usually the slowest part of the whole workflow.


Summary

Piece Handles
GitHub Actions CI workflow Type checking, linting, tests, and build, before merge
Branch protection Actually enforcing that CI passing is required, not optional
Vercel preview deployments A real, clickable URL for every pull request
Scoped environment variables Separate keys for production, preview, and local, especially Stripe and the database
Manual migration workflow Deliberate, controlled schema changes instead of automatic ones on every deploy
Rollback via Vercel dashboard A fast recovery path when something ships broken anyway

None of this is complicated infrastructure. It is closer to a checklist wired into GitHub, catching the same mistakes that used to only show up after a client noticed something broken in production.

I run this exact setup, CI checks, branch protection, scoped environment variables, on every client project with more than one person touching the code.

Get the templates: https://pixelanas.gumroad.com

What does your deploy process look like, direct to Vercel, or something with checks in front of it? Drop it below 👇


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)