Deployment

Get your SaaS running in production. Choose your platform and follow the steps.

# Prerequisites

Before deploying, make sure you have:

  • -- A GitHub repository with your project code
  • -- A PostgreSQL database (Vercel Postgres, Railway, Supabase, Neon, or self-hosted)
  • -- A Clerk account
  • -- A Stripe account
  • -- A Resend account
  • -- A custom domain (optional but recommended)

# Deploy to Vercel (Recommended)

Vercel is the easiest and fastest way to deploy your Next.js SaaS.

Step 1: Push to GitHub

git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO.git
git push -u origin main

Step 2: Import in Vercel

  1. Go to vercel.com/new
  2. Click "Import Git Repository"
  3. Select your GitHub repository
  4. Vercel auto-detects it as a Next.js project

Step 3: Add Environment Variables

In the Vercel project settings, go to Settings > Environment Variables and add every variable from the environment variables table. You can paste the contents of your .env file using the bulk editor.

Step 4: Configure Build Settings

Vercel detects these automatically, but verify:

Setting Value
Framework PresetNext.js
Build Commandnpm run build
Output Directory.next
Install Commandnpm install
Node.js Version20.x
Note: The postinstall script automatically runs prisma generate during the build.

Step 5: Deploy

Click "Deploy". Vercel will build and deploy your app. The first deploy takes 1-2 minutes.

Step 6: Custom Domain

  1. Go to Settings > Domains
  2. Add your domain (e.g., yourdomain.com)
  3. Follow the DNS configuration instructions:
Record Type Value Use
A Record76.76.21.21Root domain
CNAMEcname.vercel-dns.comSubdomains

SSL is enabled automatically.

Step 7: Post-Deploy Webhooks

After your first successful deploy, configure webhook URLs. See the Stripe Setup and Clerk Setup sections below.

# Deploy to Railway

Railway is a great alternative with built-in PostgreSQL.

Step 1: Create Project

  1. Go to railway.app and sign in
  2. Click "New Project"
  3. Select "Deploy from GitHub Repo"
  4. Choose your repository

Step 2: Add PostgreSQL

  1. In your Railway project, click "+ New"
  2. Select "Database" > "Add PostgreSQL"
  3. Railway creates the database and provides the DATABASE_URL automatically

Step 3: Configure Environment Variables

  1. Click on your app service
  2. Go to the "Variables" tab
  3. Add all the environment variables
  4. For DATABASE_URL, use the reference variable: ${{Postgres.DATABASE_URL}}

Step 4: Configure Build

Setting Value
Build Commandnpm run build
Start Commandnpm run start

Step 5: Deploy and Custom Domain

Railway deploys automatically on every push to your main branch. Add a custom domain under Settings > Networking > Custom Domain. Configure the CNAME record Railway provides. SSL is handled automatically.

# Deploy with Docker (Self-Hosted)

For full control, deploy with Docker on any VPS (DigitalOcean, Hetzner, AWS EC2, etc.).

Dockerfile

Create a Dockerfile at the project root:

# ---- Dependencies ----
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
COPY prisma ./prisma/
RUN npm ci

# ---- Build ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

# ---- Runner ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/content ./content
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]
Note: For the standalone output to work, add output: "standalone" to your next.config.ts.

docker-compose.yml

version: "3.8"

services:
  app:
    build: .
    ports:
      - "3000:3000"
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: saas
      POSTGRES_PASSWORD: your_secure_password
      POSTGRES_DB: saas_starter
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U saas"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:

Build and Run

# Build and start
docker compose up -d --build

# Run database migrations
docker compose exec app npx prisma db push

# View logs
docker compose logs -f app

# Stop
docker compose down

Nginx Reverse Proxy

Create /etc/nginx/sites-available/yourdomain.com:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com www.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Enable the site and get SSL:

sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
sudo nginx -t && sudo systemctl reload nginx

# Database Setup

Managed PostgreSQL (Recommended)

Provider Free Tier URL
Neon512 MB, auto-sleepneon.tech
Supabase500 MBsupabase.com
Vercel Postgres256 MBvercel.com
Railway$5 creditrailway.app

Self-Hosted PostgreSQL

docker run -d \
  --name postgres \
  -e POSTGRES_USER=saas \
  -e POSTGRES_PASSWORD=your_secure_password \
  -e POSTGRES_DB=saas_starter \
  -p 5432:5432 \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16-alpine

Connection string:

DATABASE_URL="postgresql://saas:your_secure_password@localhost:5432/saas_starter"

# Stripe Setup

1. Create Account and Get API Keys

  1. Go to dashboard.stripe.com and create an account
  2. Make sure Test mode is enabled (toggle in the top-right)
  3. Go to Developers > API Keys
  4. Copy the Publishable key (pk_test_...) → NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
  5. Copy the Secret key (sk_test_...) → STRIPE_SECRET_KEY

2. Create Products

Create 2 products with monthly and yearly prices each:

Product 1: Pro

  • Monthly: $29.00/monthSTRIPE_PRICE_PRO_MONTHLY
  • Yearly: $290.00/yearSTRIPE_PRICE_PRO_YEARLY

Product 2: Business

  • Monthly: $99.00/monthSTRIPE_PRICE_BUSINESS_MONTHLY
  • Yearly: $990.00/yearSTRIPE_PRICE_BUSINESS_YEARLY

3. Create Webhook Endpoint

  1. Go to Developers > Webhooks
  2. Click "+ Add endpoint"
  3. Set the endpoint URL: https://yourdomain.com/api/webhooks/stripe
  4. Select these events:
    • checkout.session.completed
    • invoice.payment_succeeded
    • customer.subscription.updated
    • customer.subscription.deleted
  5. Click "Reveal signing secret"STRIPE_WEBHOOK_SECRET

4. Test Locally with Stripe CLI

# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/api/webhooks/stripe

Test cards:

Card Number Result
4242 4242 4242 4242Successful payment
4000 0000 0000 0002Card declined
4000 0000 0000 3220Requires 3D Secure

# Clerk Setup

1. Create Application

  1. Go to dashboard.clerk.com
  2. Click "Add application"
  3. Choose sign-in methods: Email (recommended), Google (recommended), GitHub (optional)

2. Get API Keys

  1. Go to API Keys
  2. Copy the Publishable KeyNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
  3. Copy the Secret KeyCLERK_SECRET_KEY

3. Set Up Webhook

  1. Go to Webhooks in the Clerk Dashboard
  2. Click "+ Add endpoint"
  3. Set the URL: https://yourdomain.com/api/webhooks/clerk
  4. Select events: user.created, user.updated, user.deleted
  5. Copy the Signing SecretCLERK_WEBHOOK_SECRET
Note: Even without the webhook, users are synced on-demand via the syncUser() function when they access the dashboard. The webhook ensures the database is always in sync, even if the user never visits after sign-up.

# Resend Setup

1. Create Account

Go to resend.com and sign up. The free tier includes 3,000 emails/month.

2. Verify Domain

  1. Go to Domains > Add Domain
  2. Enter your domain
  3. Add the DNS records Resend provides (MX, TXT for SPF, CNAME for DKIM)
  4. Wait for verification (usually a few minutes)
Tip: Until your domain is verified, you can send from onboarding@resend.dev for testing.

3. Get API Key

  1. Go to API Keys > Create API Key
  2. Copy the key (re_...) → RESEND_API_KEY
  3. Set RESEND_FROM_EMAIL to noreply@yourdomain.com

# Post-Deploy Checklist

Run through this list after your first production deploy: