Back to Blog

CI/CD pipeline for Next.js SaaS: GitHub Actions deployment automation

CI/CD
GitHub Actions
DevOps
Next.js
Docker
Deployment
Automation

CI/CD pipelines automate building, testing, and deploying your Next.js SaaS application, reducing manual errors and enabling rapid, reliable releases. This guide covers setting up GitHub Actions workflows for automated builds, Docker image management, database migrations, and zero-downtime deployments.

Why CI/CD for Next.js SaaS?

CI/CD pipelines provide several benefits for SaaS applications:

  • Automated testing - Run tests on every commit
  • Consistent builds - Same build process across environments
  • Faster deployments - Deploy with a single click or automatically
  • Reduced errors - Automated processes eliminate manual mistakes
  • Rollback capability - Quickly revert to previous versions
  • Database migration automation - Safe, automated schema changes

Fastack implements a complete CI/CD pipeline using GitHub Actions, enabling automated builds, Docker image management, and zero-downtime deployments with database migrations.

GitHub Actions workflow structure

GitHub Actions workflows are defined in .github/workflows/ directory. A typical CI/CD pipeline includes:

Build workflow

Builds Docker images and pushes them to a container registry:

Build and push workflow
name: Build and Push App

on:
  workflow_call:
    inputs:
      app_name:
        required: true
        type: string
      version:
        required: true
        type: string

jobs:
  build-and-push:
    name: Build and Push ${{ inputs.app_name }}
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        
      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ./deploy/docker/Dockerfile.base
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/${{ inputs.app_name }}:${{ inputs.version }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

This workflow builds a Docker image for your Next.js application and pushes it to GitHub Container Registry (GHCR) with version tags. The build uses Docker Buildx for advanced features and GitHub Actions cache for faster builds.

Deploy workflow

Deploys the built image to your production server:

Deploy workflow
name: Deploy App

on:
  workflow_dispatch:
    inputs:
      app_name:
        required: true
        type: string
      version:
        required: true
        type: string
      skip_migrate:
        required: false
        type: boolean
        default: false

jobs:
  deploy:
    name: Deploy ${{ inputs.app_name }}
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        
      - name: Setup SSH
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          SSH_HOST: ${{ secrets.SSH_HOST }}
        run: |
          eval "$(ssh-agent -s)"
          echo "$SSH_PRIVATE_KEY" | ssh-add -
          ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts
          
      - name: Deploy to server
        env:
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
          APP_NAME: ${{ inputs.app_name }}
          VERSION: ${{ inputs.version }}
        run: |
          ./deploy/scripts/deploy-version.sh \
            --app "$APP_NAME" \
            --version "$VERSION" \
            ${{ inputs.skip_migrate && '--skip-migrate' || '' }}
          
      - name: Health check
        run: |
          ./deploy/scripts/health-check.sh --app "${{ inputs.app_name }}"

The deploy workflow sets up SSH access, runs deployment scripts on your server, and performs health checks to verify the deployment succeeded.

Setting up GitHub Actions secrets

Configure secrets in your GitHub repository for secure deployment:

Required secrets

Add these secrets in GitHub repository settings (Settings → Secrets and variables → Actions):

  • SSH_PRIVATE_KEY - SSH private key for server access
  • SSH_PASSPHRASE - SSH key passphrase (optional, if key is encrypted)
  • SSH_HOST - Server hostname or IP address
  • SSH_USER - SSH username for server access

For CI/CD, it's recommended to use an SSH key without a passphrase for better automation. Generate a dedicated deployment key:

Generating deployment SSH key
# Generate SSH key without passphrase for CI/CD
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_deploy -N ""

# Copy public key to server
ssh-copy-id -i ~/.ssh/id_ed25519_deploy.pub [email protected]

# Copy private key content to GitHub secret
cat ~/.ssh/id_ed25519_deploy

The private key content should be added to GitHub secrets. Never commit private keys to your repository.

Environment variables

For build-time environment variables (NEXT_PUBLIC_*), use GitHub Variables or Secrets:

Build-time environment variables
# In GitHub Actions workflow
- name: Create nextpublic-build.env file
  env:
    NEXT_PUBLIC_APP_URL: ${{ secrets.NEXT_PUBLIC_APP_URL }}
    NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}
    NEXT_PUBLIC_GA_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_GA_MEASUREMENT_ID }}
  run: |
    # Extract NEXT_PUBLIC_* variables
    for var in $(env | grep '^NEXT_PUBLIC_' | cut -d'=' -f1); do
      value=$(eval echo \$$var)
      if [ -n "$value" ]; then
        echo "${var}=${value}" >> nextpublic-build.env
      fi
    done

These variables are baked into the Docker image at build time, making them available to your Next.js application.

Docker image building and caching

Optimize Docker builds with caching and multi-stage builds:

GitHub Actions cache

Use GitHub Actions cache to speed up Docker builds:

Docker build with cache
- name: Build and push Docker image
  uses: docker/build-push-action@v5
  with:
    context: .
    file: ./deploy/docker/Dockerfile.base
    push: true
    tags: ghcr.io/${{ github.repository_owner }}/app:${{ inputs.version }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

The cache-from: type=gha and cache-to: type=gha,mode=max options use GitHub Actions cache to store Docker layer cache, significantly speeding up subsequent builds.

Version tagging

Tag Docker images with semantic versions and metadata:

Docker image tagging
- name: Extract metadata
  id: meta
  uses: docker/metadata-action@v5
  with:
    images: ghcr.io/${{ github.repository_owner }}/app
    tags: |
      type=raw,value=${{ inputs.version }}
      type=raw,value=latest,enable={{is_default_branch}}
      type=sha,prefix={{date 'YYYYMMDD'}}-,format=short

This creates multiple tags: the version tag (e.g., v1.0.0), latest for the default branch, and a date-based tag with commit SHA for traceability.

Zero-downtime deployment process

Fastack implements rolling updates for zero-downtime deployments:

  1. 1Build new Docker image and push to registry
  2. 2Export and copy image to server (compressed)
  3. 3Clean up Docker resources on server (free disk space)
  4. 4Load new Docker image on server
  5. 5Stop current container (rename to -old)
  6. 6Run database migrations (if enabled)
  7. 7Start new container (with -new suffix)
  8. 8Verify health check
  9. 9Remove old container
  10. 10Rename new container to main name

If any step fails, the old container is automatically restored, ensuring your application remains available.

Database migrations in CI/CD

Integrate Prisma migrations into your deployment pipeline:

Automatic migrations

Run migrations automatically during deployment:

Migration in deployment script
# deploy/scripts/deploy-version.sh

# Run database migrations
if [ "$SKIP_MIGRATE" != "true" ]; then
  echo "Running database migrations..."
  
  # Backup database (if enabled)
  if [ "$BACKUP" = "true" ]; then
    ./deploy/scripts/migrate.sh --app "$APP_NAME" --backup
  fi
  
  # Run migrations in temporary container
  docker run --rm \
    --env-file .env.production \
    --network host \
    ghcr.io/owner/app:$VERSION \
    npx prisma migrate deploy
  
  echo "✅ Migrations completed"
else
  echo "⏭️ Skipping migrations (--skip-migrate flag)"
fi

Migrations run in a temporary container before starting the new application container, ensuring the database schema is up-to-date before the new version starts.

Migration safety

Best practices for safe migrations in CI/CD:

  • Always backup before migrations (automatic in Fastack)
  • Test migrations in staging before production
  • Use --skip-migrate flag for emergency deployments
  • Monitor migration duration and database load

Health checks and verification

Verify deployments with automated health checks:

Health check script
# deploy/scripts/health-check.sh

HEALTH_URL="http://localhost:3000/api/health"
TIMEOUT=30
RETRIES=3

for i in $(seq 1 $RETRIES); do
  echo "Health check attempt $i/$RETRIES..."
  
  if curl -f -s --max-time $TIMEOUT "$HEALTH_URL" > /dev/null; then
    echo "✅ Health check passed"
    exit 0
  fi
  
  if [ $i -lt $RETRIES ]; then
    echo "⏳ Waiting before retry..."
    sleep 5
  fi
done

echo "❌ Health check failed after $RETRIES attempts"
exit 1

Health checks verify that the new container is running and responding correctly before removing the old container. If health checks fail, the deployment is rolled back automatically.

Workflow triggers

Configure when workflows run:

Manual deployment

Trigger deployments manually from GitHub Actions UI:

Manual workflow trigger
name: Deploy

on:
  workflow_dispatch:
    inputs:
      app_name:
        description: 'App to deploy'
        required: true
        type: choice
        options:
          - fastack
          - fastack-boilerplate
      version:
        description: 'Version tag (e.g., v1.0.0)'
        required: true
        type: string
      skip_migrate:
        description: 'Skip database migrations'
        required: false
        type: boolean
        default: false

Manual triggers allow you to deploy specific versions with optional migration skipping for emergency deployments.

Automatic deployment on push

Automatically deploy when code is pushed to specific branches:

Automatic deployment trigger
name: Deploy on Push

on:
  push:
    branches:
      - main
    paths:
      - 'apps/fastack-boilerplate/**'
      - 'packages/**'
      - 'deploy/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Get version from tag
        id: version
        run: |
          VERSION=$(git describe --tags --abbrev=0 || echo "latest")
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          
      - name: Deploy
        uses: ./.github/workflows/deploy-app.yml
        with:
          app_name: fastack-boilerplate
          version: ${{ steps.version.outputs.version }}

Automatic deployments run when changes are pushed to the main branch, but only for specific paths to avoid unnecessary deployments.

Best practices

Follow these practices for reliable CI/CD pipelines:

  • Use semantic versioning - Tag releases with version numbers (v1.0.0)
  • Test before deploy - Run tests in CI before building images
  • Use Docker layer caching - Speed up builds with GitHub Actions cache
  • Implement health checks - Verify deployments before removing old containers
  • Enable rollback - Keep previous container versions for quick rollback
  • Monitor deployments - Track deployment success rates and durations
  • Secure secrets - Use GitHub Secrets for sensitive data, never commit secrets
  • Use reusable workflows - Create workflow templates for consistency

CI/CD pipeline in Fastack

Fastack implements a complete CI/CD pipeline with:

  • Automated Docker image builds with GitHub Actions cache
  • Version-based deployments from Git tags
  • Zero-downtime rolling updates
  • Automatic database migrations with backup
  • Health check verification
  • Automatic rollback on failure
  • Reusable workflow templates for multiple apps

Fastack's CI/CD pipeline enables rapid, reliable deployments with minimal manual intervention, making it easy to ship new features and fixes to production.

Conclusion

CI/CD pipelines are essential for modern SaaS applications, enabling rapid, reliable deployments with minimal manual effort. GitHub Actions provides a powerful platform for automating builds, tests, and deployments.

Fastack's CI/CD implementation demonstrates how to structure workflows for monorepo applications, integrate Docker builds, manage database migrations, and implement zero-downtime deployments. By following these patterns, you can create a robust deployment pipeline that scales with your application.

Ready to build your SaaS faster?

Get our scalable, production-ready boilerplate to save endless hours of development and setup

CI/CD Pipeline for Next.js SaaS: GitHub Actions Deployment Automation - Fastack