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:
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=maxThis 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:
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 accessSSH_PASSPHRASE- SSH key passphrase (optional, if key is encrypted)SSH_HOST- Server hostname or IP addressSSH_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:
# 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_deployThe 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:
# 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
doneThese 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:
- 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=maxThe 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:
- 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=shortThis 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:
- 1Build new Docker image and push to registry
- 2Export and copy image to server (compressed)
- 3Clean up Docker resources on server (free disk space)
- 4Load new Docker image on server
- 5Stop current container (rename to
-old) - 6Run database migrations (if enabled)
- 7Start new container (with
-newsuffix) - 8Verify health check
- 9Remove old container
- 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:
# 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)"
fiMigrations 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-migrateflag for emergency deployments - Monitor migration duration and database load
Health checks and verification
Verify deployments with automated health checks:
# 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 1Health 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:
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: falseManual 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:
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
