Overview
The standard deployment pipeline for Next.js SaaS: GitHub as source of truth, Vercel as host, GitHub Actions for CI gates. Preview deployments automatically on every PR.
Initial Setup
1. Create GitHub Repository
# Via GitHub CLI
gh repo create my-app --private --source=. --remote=origin --push
# Or manual
git init && git add -A && git commit -m "init"
git remote add origin https://github.com/username/my-app.git
git push -u origin main
2. Link Vercel Project
npm i -g vercel
vercel link # follow prompts to connect to Vercel account and project
This creates .vercel/project.json with your project ID and org ID — needed for CI.
3. Connect GitHub to Vercel
In Vercel Dashboard → Project → Settings → Git:
- Connect GitHub repository
- Set production branch:
main - Enable preview deployments for all branches
GitHub Actions CI Pipeline
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: "20"
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- run: npm ci
- name: Deploy to Vercel
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: |
npx vercel pull --yes --environment=production --token=$VERCEL_TOKEN
npx vercel build --prod --token=$VERCEL_TOKEN
npx vercel deploy --prebuilt --prod --token=$VERCEL_TOKEN
Required GitHub Secrets
In GitHub repo → Settings → Secrets and Variables → Actions:
| Secret | Where to get it |
|--------|----------------|
| VERCEL_TOKEN | vercel.com/account/tokens |
| VERCEL_ORG_ID | .vercel/project.json → orgId |
| VERCEL_PROJECT_ID | .vercel/project.json → projectId |
Preview Deployments
Every PR automatically gets a preview URL from Vercel:
- Format:
https://my-app-git-branch-username.vercel.app - Shares production env vars (unless you configure separate preview vars)
- Deleted automatically when PR is merged or closed
To set preview-specific env vars:
vercel env add DATABASE_URL preview
# or in Dashboard → Settings → Environment Variables → set Target: Preview
Branch-Based Environments
For staging vs production:
# In Vercel Dashboard → Settings → Git
# Add custom build settings per branch:
# Branch: staging → Build command: npm run build:staging
# Branch: main → Build command: npm run build
Or set branch-specific env vars:
vercel env add API_URL preview # staging API
vercel env add API_URL production # production API
Environment Variable Sync
# Pull current production env to local
vercel env pull .env.local
# Add new var to specific targets
vercel env add NEW_SECRET production
vercel env add NEW_SECRET preview
# List all vars
vercel env ls
# Remove a var
vercel env rm OLD_SECRET production
Rollback
# List recent deployments
vercel ls
# Rollback to specific deployment
vercel rollback <deployment-url>
# Or via Dashboard: Deployments → find previous → Promote to Production
Custom Domain Setup
# Add domain
vercel domains add yourdomain.com
# Check DNS requirements
vercel domains inspect yourdomain.com
DNS records (add in your registrar):
- Root:
A 76.76.21.21 - www:
CNAME cname.vercel-dns.com
SSL certificate auto-provisioned after DNS propagates (usually < 5 min).
Monorepo Setup
For monorepos (multiple apps in one repo):
// vercel.json (in app subfolder)
{
"buildCommand": "cd ../.. && npm run build --workspace=apps/web",
"outputDirectory": "apps/web/.next",
"installCommand": "npm install"
}
In Vercel Dashboard, set Root Directory to apps/web.
Build Cache
Vercel caches .next/cache between deployments. To invalidate:
vercel deploy --force # bypasses cache
Health Check Endpoint
Add to src/app/api/health/route.ts:
export const dynamic = "force-dynamic";
export async function GET() {
return Response.json({
status: "ok",
timestamp: new Date().toISOString(),
version: process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7) ?? "local",
});
}
Vercel exposes VERCEL_GIT_COMMIT_SHA, VERCEL_URL, VERCEL_ENV automatically.