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