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
- Go to vercel.com/new
- Click "Import Git Repository"
- Select your GitHub repository
- 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 Preset | Next.js |
| Build Command | npm run build |
| Output Directory | .next |
| Install Command | npm install |
| Node.js Version | 20.x |
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
- Go to Settings > Domains
- Add your domain (e.g.,
yourdomain.com) - Follow the DNS configuration instructions:
| Record Type | Value | Use |
|---|---|---|
| A Record | 76.76.21.21 | Root domain |
| CNAME | cname.vercel-dns.com | Subdomains |
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
- Go to railway.app and sign in
- Click "New Project"
- Select "Deploy from GitHub Repo"
- Choose your repository
Step 2: Add PostgreSQL
- In your Railway project, click "+ New"
- Select "Database" > "Add PostgreSQL"
- Railway creates the database and provides the
DATABASE_URLautomatically
Step 3: Configure Environment Variables
- Click on your app service
- Go to the "Variables" tab
- Add all the environment variables
- For
DATABASE_URL, use the reference variable:${{Postgres.DATABASE_URL}}
Step 4: Configure Build
| Setting | Value |
|---|---|
| Build Command | npm run build |
| Start Command | npm 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"]
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 |
|---|---|---|
| Neon | 512 MB, auto-sleep | neon.tech |
| Supabase | 500 MB | supabase.com |
| Vercel Postgres | 256 MB | vercel.com |
| Railway | $5 credit | railway.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
- Go to dashboard.stripe.com and create an account
- Make sure Test mode is enabled (toggle in the top-right)
- Go to Developers > API Keys
- Copy the Publishable key (
pk_test_...) →NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY - 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/month →
STRIPE_PRICE_PRO_MONTHLY - Yearly: $290.00/year →
STRIPE_PRICE_PRO_YEARLY
Product 2: Business
- Monthly: $99.00/month →
STRIPE_PRICE_BUSINESS_MONTHLY - Yearly: $990.00/year →
STRIPE_PRICE_BUSINESS_YEARLY
3. Create Webhook Endpoint
- Go to Developers > Webhooks
- Click "+ Add endpoint"
- Set the endpoint URL:
https://yourdomain.com/api/webhooks/stripe - Select these events:
checkout.session.completedinvoice.payment_succeededcustomer.subscription.updatedcustomer.subscription.deleted
- 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 4242 | Successful payment |
4000 0000 0000 0002 | Card declined |
4000 0000 0000 3220 | Requires 3D Secure |
# Clerk Setup
1. Create Application
- Go to dashboard.clerk.com
- Click "Add application"
- Choose sign-in methods: Email (recommended), Google (recommended), GitHub (optional)
2. Get API Keys
- Go to API Keys
- Copy the Publishable Key →
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY - Copy the Secret Key →
CLERK_SECRET_KEY
3. Set Up Webhook
- Go to Webhooks in the Clerk Dashboard
- Click "+ Add endpoint"
- Set the URL:
https://yourdomain.com/api/webhooks/clerk - Select events:
user.created,user.updated,user.deleted - Copy the Signing Secret →
CLERK_WEBHOOK_SECRET
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
- Go to Domains > Add Domain
- Enter your domain
- Add the DNS records Resend provides (MX, TXT for SPF, CNAME for DKIM)
- Wait for verification (usually a few minutes)
onboarding@resend.dev for testing.
3. Get API Key
- Go to API Keys > Create API Key
- Copy the key (
re_...) →RESEND_API_KEY - Set
RESEND_FROM_EMAILtonoreply@yourdomain.com
# Post-Deploy Checklist
Run through this list after your first production deploy: