35% OFF
Ends14d 00:00:00
Shop
Back to Blog
nextjsdevopstypescriptwebdev

Next.js CI/CD with GitHub Actions and Vercel

Pixel Anas·Updated ·6 min read

Set up Next.js checks in GitHub Actions, preview deployments, separate environment variables, and a controlled production release workflow.

For a long time deployment meant pushing to main and hoping nothing broke. No preview environment, no automated checks, just a direct line from my laptop to production. One bad push later, that stopped being an acceptable way to work.

Here is the deployment setup I actually use now, across solo projects and client work.


1. Vercel as the Default, and Why

For most Next.js projects, Vercel remains the simplest deploy target since it is built by the same team as the framework, meaning App Router features, Server Actions, and edge functions all work without extra configuration most other hosts need.

npm install -g vercel
vercel login
vercel link

Connecting a GitHub repo through the Vercel dashboard is usually enough to get automatic deployments on every push, no CI config required for the basic case. The setup below adds the layer on top of that: checks that must pass before a deployment is trusted.


2. Preview Deployments for Every Pull Request

This is the single most useful thing Vercel does automatically. Every pull request gets its own live URL, a real deployed version of that branch, not just a diff to review.

https://your-project-git-feature-branch-yourteam.vercel.app

Reviewing a pull request by clicking through the actual running app, instead of just reading a diff, catches visual and interaction bugs that code review alone misses. This happens automatically once the repo is connected, no extra configuration needed.


3. Running Checks Before Deployment with GitHub Actions

Vercel deploys on every push by default, checks or no checks. A separate GitHub Actions workflow does not automatically block Vercel preview deployments. Require successful checks before merging to the production branch, or configure an explicit deployment gate if the deployment itself must wait.

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

on:
  pull_request:
    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

      - name: Build
        run: npm run build

The example assumes lint and test scripts exist in package.json; define project-appropriate commands first.

npm ci instead of npm install matters in CI specifically, it installs exactly what is in package-lock.json with no version drift, and fails outright if the lockfile and package.json are out of sync, which catches a whole class of "works on my machine" issues.


4. Blocking Merges on Failed Checks

The workflow above runs, but does nothing to stop a merge unless it is actually required. In the GitHub repo settings, under branch protection rules for main, require the checks job to pass before merging is allowed.

Without this step, the CI workflow is informational only, someone can see a red X and merge anyway. Making it a required check turns it into an actual gate.


5. Environment Variables Across Environments

This is where I have seen the most real mistakes happen, a development database URI accidentally used in production, or a Stripe test key shipped live. Vercel supports separate variables per environment, and it is worth actually using that separation.

# In the Vercel dashboard, set separately for each environment:

Production:
  MONGODB_URI=mongodb+srv://prod-cluster...
  STRIPE_SECRET_KEY=sk_live_...

Preview:
  MONGODB_URI=mongodb+srv://staging-cluster...
  STRIPE_SECRET_KEY=sk_test_...

Development:
  MONGODB_URI=mongodb+srv://dev-cluster...
  STRIPE_SECRET_KEY=sk_test_...

Preview deployments (from pull requests) should never point at the production database or live payment keys. A bug in a feature branch should not be able to touch real customer data, and testing a Stripe flow on a preview URL should never risk a real charge.


6. Validating Environment Variables at Build Time

A missing environment variable failing silently in production, rather than loudly at build time, is a specific kind of bad. Catching it during the build instead means a bad deploy never goes live in the first place.

// lib/env.ts
const requiredEnvVars = [
  'MONGODB_URI',
  'JWT_SECRET',
  'STRIPE_SECRET_KEY',
] as const;

for (const key of requiredEnvVars) {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
}
// next.config.ts
import './lib/env';

const nextConfig = {
  // your config
};

export default nextConfig;

Importing this at the top of next.config.ts means the build fails immediately if a required variable is missing, instead of deploying successfully and failing later at runtime when a real request hits the missing config.


7. Run Database Changes as a Controlled Release Step

Do not run production database mutations inside every framework build. Preview builds, retries, and parallel deployments can run unexpectedly. Use a deliberate, authenticated release job against the intended database, with a backup and recovery plan.

Prefer backward-compatible changes so the current and next app versions can coexist during rollout. Test the migration on representative non-production data and verify it before relying on the new schema. A code rollback does not undo database changes.

8. Rolling Back a Bad Deploy

Even with checks in place, something eventually ships broken. Vercel keeps every previous deployment live and instantly promotable back to production, no rebuild required.

vercel ls
vercel promote <deployment-url>

This is faster than reverting a commit and waiting for a fresh build, since the previous working deployment is already built and sitting there. Reverting the commit in git afterward keeps the repo history accurate, but the promote step is what actually stops the bleeding immediately.


Summary

PieceSolves
Vercel + GitHub integrationAutomatic deployments, no manual deploy steps
Preview deployments per PRReviewing a real running app, not just a diff
GitHub Actions CI workflowCatching type errors, lint issues, and failing tests before merge
Required status checksTurning CI from informational into an actual merge gate
Separate env vars per environmentPreview and dev never touching production data or live keys
Build-time env validationFailing loud at build time instead of silent at runtime
Instant rollback via promoteFast recovery from a bad deploy without waiting on a rebuild

None of this is complicated individually. What it adds up to is the difference between finding out something broke from a failed check versus finding out from a client message after it already broke in production.

I run this exact setup, CI checks, environment separation, preview deployments, across every client project and template I ship.

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

What does your deployment pipeline look like, manual pushes or a full CI setup? Drop it below 👇


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

Advertisement

Chat with me on WhatsApp