DEV Community

Cover image for GitHub Actions workflow to deploy the Next.js Application on Vercel Cloud
Latchu@DevOps
Latchu@DevOps

Posted on

GitHub Actions workflow to deploy the Next.js Application on Vercel Cloud

In this article, I’ll walk you through setting up a GitHub Actions workflow to automatically deploy a Next.js application to Vercel.
Continuous deployment helps streamline development and ensures every push is production-ready.
We’ll configure secrets, create a workflow file, and connect GitHub with Vercel for seamless deployments.
This setup enables automated builds and previews for every commit.
Let’s dive into building a simple CI/CD pipeline for your Next.js app.


What We'll Build

A complete Next.js application that:

  • ✅ Runs on Ubuntu 24 on port 80 as a background service
  • ✅ Uses TypeScript and Tailwind CSS
  • ✅ Has automated testing with Jest
  • ✅ Auto-deploys to Vercel on every push
  • ✅ Includes GitHub Actions CI/CD pipeline

Tech Stack

  • Next.js 14 (App Router)
  • TypeScript
  • Tailwind CSS
  • PM2 (Process Manager)
  • GitHub Actions
  • Vercel

📋 Prerequisites

  • Ubuntu 24.04 server (local or cloud)
  • GitHub account
  • Vercel account (free)

- Basic knowledge of Git and terminal

STEP 1: Install Node.js and Required Tools on local machine

# Update system
sudo apt update && sudo apt upgrade -y

# Install Node.js 20.x (LTS)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Verify installation
node --version   # Should show v20.x.x
npm --version    # Should show 10.x.x

# Install additional tools
sudo apt install -y git curl build-essential

# Install PM2 (process manager for background running)
sudo npm install -g pm2

# Verify PM2
pm2 --version
Enter fullscreen mode Exit fullscreen mode

STEP 2: Create Next.js Application

# Create project directory
mkdir -p ~/nextjs-app
cd ~/nextjs-app

# Create Next.js app structure
# We'll create all files manually for complete control
Enter fullscreen mode Exit fullscreen mode

Create Project Structure

# Create directories
mkdir -p app/{api/hello,components,lib,styles}
mkdir -p public/images
mkdir -p .github/workflows

# Create files
touch next.config.js
touch package.json
touch tsconfig.json
touch .gitignore
touch .env.local
touch README.md
touch ecosystem.config.js
Enter fullscreen mode Exit fullscreen mode

Now, let's create each file with content:


File 1: package.json

cat > package.json << 'EOF'
{
  "name": "nextjs-app",
  "version": "1.0.0",
  "description": "Complete Next.js application with CI/CD",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start -p 80",
    "lint": "next lint",
    "test": "jest",
    "test:watch": "jest --watch"
  },
  "dependencies": {
    "next": "14.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.1.5",
    "@testing-library/react": "^14.1.2",
    "@types/node": "^20.10.6",
    "@types/react": "^18.2.46",
    "@types/react-dom": "^18.2.18",
    "autoprefixer": "^10.4.16",
    "eslint": "^8.56.0",
    "eslint-config-next": "14.1.0",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "postcss": "^8.4.32",
    "tailwindcss": "^3.4.0",
    "typescript": "^5.3.3"
  }
}
EOF
Enter fullscreen mode Exit fullscreen mode

File 2: next.config.js

cat > next.config.js << 'EOF'
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  output: 'standalone',
  poweredByHeader: false,

  // Environment variables
  env: {
    NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME || 'Next.js App',
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:80',
  },

  // Image optimization
  images: {
    domains: ['localhost'],
    formats: ['image/avif', 'image/webp'],
  },

  // Headers
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'Referrer-Policy',
            value: 'origin-when-cross-origin',
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;
EOF
Enter fullscreen mode Exit fullscreen mode

File 3: tsconfig.json

cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
EOF
Enter fullscreen mode Exit fullscreen mode

File 4: .gitignore

cat > .gitignore << 'EOF'
# Dependencies
node_modules/
/.pnp
.pnp.js

# Testing
/coverage

# Next.js
/.next/
/out/
.next

# Production
/build
dist

# Misc
.DS_Store
*.pem
.vercel

# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local env files
.env*.local
.env.production

# PM2
.pm2

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# Logs
logs
*.log
EOF
Enter fullscreen mode Exit fullscreen mode

File 5: .env.local

cat > .env.local << 'EOF'
# Application
NEXT_PUBLIC_APP_NAME=My Next.js App
NEXT_PUBLIC_API_URL=http://localhost:80

# Environment
NODE_ENV=development
EOF
Enter fullscreen mode Exit fullscreen mode

File 6: app/layout.tsx

cat > app/layout.tsx << 'EOF'
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'Next.js App',
  description: 'Complete Next.js application with CI/CD',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
EOF
Enter fullscreen mode Exit fullscreen mode

File 7: app/page.tsx

cat > app/page.tsx << 'EOF'
import Link from 'next/link'

export default function Home() {
  return (
    <main className="min-h-screen flex flex-col items-center justify-center p-24 bg-gradient-to-br from-blue-50 to-indigo-100">
      <div className="max-w-4xl w-full space-y-8 text-center">
        <div className="space-y-4">
          <h1 className="text-6xl font-bold text-gray-900">
            Welcome to <span className="text-blue-600">Next.js</span>
          </h1>
          <p className="text-xl text-gray-600">
            A complete Next.js application with CI/CD pipeline
          </p>
        </div>

        <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-12">
          <div className="p-6 border border-gray-200 rounded-lg bg-white shadow-sm hover:shadow-md transition-shadow">
            <h3 className="text-xl font-semibold mb-2">📦 Features</h3>
            <p className="text-gray-600">
              TypeScript, Tailwind CSS, API Routes
            </p>
          </div>

          <div className="p-6 border border-gray-200 rounded-lg bg-white shadow-sm hover:shadow-md transition-shadow">
            <h3 className="text-xl font-semibold mb-2">🚀 Deploy</h3>
            <p className="text-gray-600">
              Automated deployment to Vercel
            </p>
          </div>

          <div className="p-6 border border-gray-200 rounded-lg bg-white shadow-sm hover:shadow-md transition-shadow">
            <h3 className="text-xl font-semibold mb-2">🔧 CI/CD</h3>
            <p className="text-gray-600">
              GitHub Actions workflow included
            </p>
          </div>
        </div>

        <div className="mt-12 space-x-4">
          <Link
            href="/about"
            className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
          >
            About Page
          </Link>
          <Link
            href="/api/hello"
            className="inline-block px-6 py-3 bg-gray-800 text-white rounded-lg hover:bg-gray-900 transition-colors"
          >
            Test API
          </Link>
        </div>

        <div className="mt-8 text-sm text-gray-500">
          <p>Running on port 80 • Managed by PM2</p>
        </div>
      </div>
    </main>
  )
}
EOF
Enter fullscreen mode Exit fullscreen mode

File 8: app/about/page.tsx

mkdir -p app/about
cat > app/about/page.tsx << 'EOF'
import Link from 'next/link'

export default function About() {
  return (
    <main className="min-h-screen flex flex-col items-center justify-center p-24 bg-gradient-to-br from-purple-50 to-pink-100">
      <div className="max-w-2xl w-full space-y-8">
        <h1 className="text-5xl font-bold text-gray-900 text-center">
          About This App
        </h1>

        <div className="bg-white p-8 rounded-lg shadow-md space-y-4">
          <h2 className="text-2xl font-semibold text-gray-800">Features</h2>
          <ul className="list-disc list-inside space-y-2 text-gray-600">
            <li>Next.js 14 with App Router</li>
            <li>TypeScript for type safety</li>
            <li>Tailwind CSS for styling</li>
            <li>API Routes for backend logic</li>
            <li>PM2 for process management</li>
            <li>GitHub Actions for CI/CD</li>
            <li>Vercel deployment ready</li>
          </ul>
        </div>

        <div className="text-center">
          <Link
            href="/"
            className="inline-block px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
          >
            ← Back to Home
          </Link>
        </div>
      </div>
    </main>
  )
}
EOF
Enter fullscreen mode Exit fullscreen mode

File 9: app/api/hello/route.ts

cat > app/api/hello/route.ts << 'EOF'
import { NextResponse } from 'next/server'

export async function GET() {
  return NextResponse.json({
    message: 'Hello from Next.js API!',
    timestamp: new Date().toISOString(),
    status: 'success',
    environment: process.env.NODE_ENV,
  })
}

export async function POST(request: Request) {
  const body = await request.json()

  return NextResponse.json({
    message: 'Data received successfully',
    receivedData: body,
    timestamp: new Date().toISOString(),
  })
}
EOF
Enter fullscreen mode Exit fullscreen mode

File 10: app/globals.css

cat > app/globals.css << 'EOF'
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0;
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(
      to bottom,
      transparent,
      rgb(var(--background-end-rgb))
    )
    rgb(var(--background-start-rgb));
}
EOF
Enter fullscreen mode Exit fullscreen mode

File 11: tailwind.config.ts

cat > tailwind.config.ts << 'EOF'
import type { Config } from 'tailwindcss'

const config: Config = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      backgroundImage: {
        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
        'gradient-conic':
          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
      },
    },
  },
  plugins: [],
}
export default config
EOF
Enter fullscreen mode Exit fullscreen mode

File 12: postcss.config.js

cat > postcss.config.js << 'EOF'
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}
EOF
Enter fullscreen mode Exit fullscreen mode

File 13: jest.config.js

cat > jest.config.js << 'EOF'
const nextJest = require('next/jest')

const createJestConfig = nextJest({
  dir: './',
})

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/$1',
  },
  testMatch: [
    '**/__tests__/**/*.[jt]s?(x)',
    '**/?(*.)+(spec|test).[jt]s?(x)',
  ],
}

module.exports = createJestConfig(customJestConfig)
EOF
Enter fullscreen mode Exit fullscreen mode

File 14: jest.setup.js

cat > jest.setup.js << 'EOF'
import '@testing-library/jest-dom'
EOF
Enter fullscreen mode Exit fullscreen mode

File 15: tests/page.test.tsx

mkdir -p __tests__
cat > __tests__/page.test.tsx << 'EOF'
import { render, screen } from '@testing-library/react'
import Home from '@/app/page'

describe('Home Page', () => {
  it('renders the main heading', () => {
    render(<Home />)
    const heading = screen.getByRole('heading', { level: 1 })
    expect(heading).toBeInTheDocument()
  })

  it('contains welcome text', () => {
    render(<Home />)
    expect(screen.getByText(/Welcome to/i)).toBeInTheDocument()
  })
})
EOF
Enter fullscreen mode Exit fullscreen mode

File 16: ecosystem.config.js (PM2 Configuration)

cat > ecosystem.config.js << 'EOF'
module.exports = {
  apps: [
    {
      name: 'nextjs-app',
      script: 'npm',
      args: 'start',
      cwd: './',
      instances: 1,
      autorestart: true,
      watch: false,
      max_memory_restart: '1G',
      env: {
        NODE_ENV: 'production',
        PORT: 80,
      },
      error_file: './logs/err.log',
      out_file: './logs/out.log',
      log_file: './logs/combined.log',
      time: true,
    },
  ],
}
EOF

# Create logs directory
mkdir -p logs
Enter fullscreen mode Exit fullscreen mode

File 17: README.md

cat > README.md << 'EOF'
# Next.js Application

Complete Next.js application with CI/CD pipeline.

## Features

- ✅ Next.js 14 with App Router
- ✅ TypeScript
- ✅ Tailwind CSS
- ✅ API Routes
- ✅ Jest Testing
- ✅ PM2 Process Management
- ✅ GitHub Actions CI/CD
- ✅ Vercel Deployment

## Development

npm install
npm run dev


## Production

npm run build
npm start


## Testing

npm test


## Deployment

Automatically deploys to Vercel on push to main branch.
EOF
Enter fullscreen mode Exit fullscreen mode

STEP 3: Install Dependencies

# Install all dependencies
npm install

# This will take a few minutes
Enter fullscreen mode Exit fullscreen mode

STEP 4: Build the Application

# Build for production
npm run build

# Expected output:
# ✓ Compiled successfully
# ✓ Linting and checking validity of types
# ✓ Collecting page data
# ✓ Generating static pages
Enter fullscreen mode Exit fullscreen mode

STEP 5: Run on Port 80 (Requires sudo)

Using PM2

# Build first
npm run build

# Give Node.js permission to bind to port 80
sudo setcap 'cap_net_bind_service=+ep' $(which node)

# Start with PM2
pm2 start ecosystem.config.js

# Check status
pm2 status

# View logs
pm2 logs nextjs-app

# Save PM2 process list
pm2 save

# Setup PM2 to start on system boot
pm2 startup
# Follow the command it outputs
Enter fullscreen mode Exit fullscreen mode

STEP 6: Test the Application

# Test locally
curl http://localhost:80

# Or open in browser
# http://localhost:80
Enter fullscreen mode Exit fullscreen mode

STEP 7: Push to GitHub

# Initialize git
git init

# Add all files
git add .

# Commit
git commit -m "Initial commit: Complete Next.js app with CI/CD"

# Add remote (replace with your repo)
git remote add origin git@github.com:YOUR_USERNAME/nextjs-app.git

# Push
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

STEP 8: Create GitHub Actions Workflow

# Create workflow file
cat > .github/workflows/deploy.yml << 'EOF'
name: CI/CD Pipeline

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

env:
  NODE_VERSION: '20.x'

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: test

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build application
        run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: nextjs-build
          path: .next
          retention-days: 7

  deploy:
    name: Deploy to Vercel
    runs-on: ubuntu-latest
    needs: [test, build]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'
EOF

# Commit and push
git add .github/workflows/deploy.yml
git commit -m "Add GitHub Actions CI/CD workflow"
git push origin main
Enter fullscreen mode Exit fullscreen mode

STEP 9: Setup Vercel

9.1: Install Vercel CLI

npm install -g vercel
Enter fullscreen mode Exit fullscreen mode

9.2: Login to Vercel

vercel login

1. Select "Continue with GitHub"
2. Vercel will open a browser window
3. Browser URL: https://vercel.com/auth/github
4. Click "Authorize Vercel"
5. You'll see: "Successfully logged in!"
6. Return to terminal
7. Terminal shows: ✅ Success! GitHub authentication complete
Enter fullscreen mode Exit fullscreen mode

9.3: Link Project to Vercel

vercel link
Enter fullscreen mode Exit fullscreen mode

## Follow These Prompts Exactly:


### Prompt 1:

? Set up "~/nextjs-app"? (Y/n)


**Answer:** Type `y` or just press **Enter** (default is Yes)

### Prompt 2

? Which scope should contain your project?
  ❯ yourname (Personal Account)
    your-team (Team Account)


**Answer:** Use arrow keys to select your account, then press **Enter**


### Prompt 3

? Link to existing project? (y/N)


**Answer:** Type `n` or just press **Enter** (we're creating new project)



### Prompt 4

? What's your project's name? (nextjs-app)


**Answer:** Press **Enter** (keep default name) or type a custom name



### Prompt 5

? In which directory is your code located? (./)


**Answer:** Press **Enter** (keep default `./`)



## Expected Success Output

✅ Linked to yourname/nextjs-app (created .vercel and added it to .gitignore)
Enter fullscreen mode Exit fullscreen mode

9.4: Get Vercel Credentials

# Get Vercel Token
# Go to: https://vercel.com/account/tokens
# Click "Create Token"
# Name: GitHub Actions
# Scope: Full Account
# Copy the token

# Get Project ID and Org ID
cat .vercel/project.json
Enter fullscreen mode Exit fullscreen mode

Output will look like:

{
  "projectId": "prj_abc123xyz",
  "orgId": "team_xyz789abc"
}
Enter fullscreen mode Exit fullscreen mode

STEP 10: Add GitHub Secrets

  1. Go to: https://github.com/YOUR_USERNAME/nextjs-app

  2. Click: Settings → Secrets and variables → Actions

  3. Click: "New repository secret"

  4. Add three secrets:

| Secret Name        | Value Description            | Where to Get It                              |
|--------------------|-----------------------------|----------------------------------------------|
| VERCEL_TOKEN       | Your Vercel personal token  | https://vercel.com/account/tokens            |
| VERCEL_ORG_ID      | Vercel Organization ID      | From `.vercel/project.json` → `cat .vercel/project.json` |
| VERCEL_PROJECT_ID  | Vercel Project ID           | From `.vercel/project.json` → `cat .vercel/project.json` |

Enter fullscreen mode Exit fullscreen mode

STEP 11: Test Full Pipeline

# Make a change
echo "# Test deployment" >> README.md

# Commit and push
git add README.md
git commit -m "Test CI/CD pipeline"
git push origin main

# Watch workflow
# Go to: GitHub → Actions tab
# Watch the pipeline run:
#   ✅ Test
#   ✅ Build  
#   ✅ Deploy to Vercel
Enter fullscreen mode Exit fullscreen mode

🎨 How to Make Changes and See Automatic Deployment

Understanding the Workflow

Local Machine (Ubuntu)
    ↓ (make changes)
    ↓ (git commit)
    ↓ (git push)
    ↓
GitHub Repository
    ↓ (triggers)
    ↓
GitHub Actions (if configured)
    ↓ (or directly)
    ↓
Vercel Deployment
    ↓ (automatic)
    ↓
Live Website Updated ✅
Enter fullscreen mode Exit fullscreen mode

🚀 Quick Test: Make Your First Change

Change Homepage Text (Easiest)

# Navigate to project
cd ~/nextjs-app

# Edit the home page
nano app/page.tsx
Enter fullscreen mode Exit fullscreen mode

📤 Push Changes to GitHub

# Check what changed
git status

# Stage the changes
git add app/page.tsx

# Commit with a message
git commit -m "Update homepage: Changed title and description"

# Push to GitHub
git push origin main
Enter fullscreen mode Exit fullscreen mode

🔍 Watch the Automatic Deployment

Method 1: Watch on Vercel Dashboard

1. Open browser
2. Go to: https://vercel.com/dashboard
3. Click on your project: sowmiya-next-js-app
4. You'll see: "Building..." 
5. Wait 30-60 seconds
6. Status changes to: "Ready" ✅
7. Click "Visit" to see your changes live!
Enter fullscreen mode Exit fullscreen mode

1


🌐 View Your Changes

2


Top comments (0)