Deployment Guide
Deploy ProductReady to production - Vercel, Docker, environment setup, and CI/CD best practices
Deployment Guide
Complete guide to deploying ProductReady to production. Learn deployment strategies for Vercel, Docker, and self-hosted environments.
Recommended: Deploy to Vercel for the easiest experience. It's optimized for Next.js and requires minimal configuration.
Quick Overview
Deployment options:
- ✅ Vercel - Recommended (1-click deploy, automatic HTTPS)
- ✅ Docker - Self-hosted or cloud (AWS, GCP, Azure)
- ✅ Kubernetes - For large-scale applications
- ✅ VPS - Traditional server (DigitalOcean, Linode)
What you'll need:
- PostgreSQL database (Neon, Supabase, or self-hosted)
- Email provider (Resend, Postmark, AWS SES)
- Payment provider (Stripe, LemonSqueezy) - optional
- Domain name + SSL certificate
Deploy to Vercel
One-Click Deploy
The fastest way to get started:
Fork Repository
Fork ProductReady to your GitHub account
Import to Vercel
- Go to vercel.com/new
- Import your forked repository
- Select
apps/productreadyas root directory
Configure Environment Variables
Add these in Vercel dashboard:
# Database
PG_DATABASE_URL=postgresql://user:pass@host:5432/db
# Auth
BETTER_AUTH_SECRET=your-32-char-secret
BETTER_AUTH_URL=https://your-domain.vercel.app
# OAuth (optional)
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-secret
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-secret
# Email (optional)
RESEND_API_KEY=re_123456789
# Payment (optional)
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...Deploy
Click Deploy - Vercel will:
- Install dependencies
- Build your app
- Deploy to production
- Provide a URL
Custom Domain
Add domain in Vercel:
- Go to Project Settings → Domains
- Add your domain (e.g.,
productready.com)
Update DNS:
- Add CNAME record pointing to
cname.vercel-dns.com - Wait for DNS propagation (usually < 5 minutes)
Update environment variables:
BETTER_AUTH_URL=https://productready.comEnvironment-Specific Variables
Vercel supports different environments:
# Production only
STRIPE_SECRET_KEY=sk_live_...
# Preview (PR deployments)
STRIPE_SECRET_KEY=sk_test_...
# Development
STRIPE_SECRET_KEY=sk_test_...Deploy with Docker
Dockerfile
ProductReady includes a production-ready Dockerfile:
# apps/productready/Dockerfile
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install pnpm
RUN npm install -g pnpm
# Copy package files
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build
ENV NEXT_TELEMETRY_DISABLED 1
RUN pnpm build
# Production image
FROM base 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 built files
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]Build & Run
# Build image
docker build -t productready:latest .
# Run container
docker run -p 3000:3000 \
-e PG_DATABASE_URL="postgresql://..." \
-e BETTER_AUTH_SECRET="your-secret" \
-e BETTER_AUTH_URL="https://your-domain.com" \
productready:latestDocker Compose
For local development or simple deployments:
# docker-compose.yml
version: '3.9'
services:
app:
build: .
ports:
- '3000:3000'
environment:
- PG_DATABASE_URL=postgresql://postgres:postgres@db:5432/productready
- BETTER_AUTH_SECRET=your-32-char-secret
- BETTER_AUTH_URL=http://localhost:3000
depends_on:
- db
db:
image: postgres:16-alpine
environment:
- POSTGRES_DB=productready
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- '5432:5432'
volumes:
postgres_data:Run with:
docker-compose up -dDeploy to Kubernetes
Kubernetes Manifests
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: productready
spec:
replicas: 3
selector:
matchLabels:
app: productready
template:
metadata:
labels:
app: productready
spec:
containers:
- name: productready
image: your-registry/productready:latest
ports:
- containerPort: 3000
env:
- name: PG_DATABASE_URL
valueFrom:
secretKeyRef:
name: productready-secrets
key: database-url
- name: BETTER_AUTH_SECRET
valueFrom:
secretKeyRef:
name: productready-secrets
key: auth-secret
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: productready-service
spec:
selector:
app: productready
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: LoadBalancerSecrets
# k8s/secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: productready-secrets
type: Opaque
stringData:
database-url: postgresql://user:pass@host:5432/db
auth-secret: your-32-char-secretApply:
kubectl apply -f k8s/secrets.yaml
kubectl apply -f k8s/deployment.yamlDatabase Setup
Neon (Recommended)
- Create account at neon.tech
- Create project
- Copy connection string
- Add to environment:
PG_DATABASE_URL=postgresql://user:pass@host.neon.tech:5432/neondb?sslmode=requireWhy Neon?
- ✅ Serverless PostgreSQL (scales to zero)
- ✅ Generous free tier
- ✅ Branch databases for development
- ✅ Built-in connection pooling
Supabase
- Create project at supabase.com
- Get connection string from Settings → Database
- Use connection pooling string for production:
# Transaction mode (recommended for Drizzle)
PG_DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgresLocal Database
For local development, ProductReady provides a Docker Compose configuration to quickly start all required database services.
Start Local Database with make db
Run from the project root directory (kapps/):
# Start database services
make dbThis starts:
-
PostgreSQL 16 (with pgvector extension)
- Port:
5432 - User:
bika - Password:
bikabika - Database:
bikadev - Connection string:
postgresql://bika:bikabika@localhost:5432/bikadev
- Port:
-
Redis Stack 7.2 (with Redis Insight)
- Redis port:
6379 - Redis Insight UI:
http://localhost:8001 - Data persistence:
./.data/redis
- Redis port:
-
Typesense 27.1 (search engine)
- Port:
8108 - API Key:
bikabika - Data persistence:
./.data/typesense
- Port:
Common Database Management Commands
# Stop database services
make db-stop
# View service logs
make db-logs
# Run database migrations (all projects)
make db-migrate
# Generate Prisma/Drizzle clients
make db-generate
# Seed database with test data
make db-seed
# Reset database
make db-resetConfigure Environment Variables
In apps/productready/.env, configure the local database connection:
# PostgreSQL
PG_DATABASE_URL="postgresql://bika:bikabika@localhost:5432/bikadev"
# Redis (if needed)
REDIS_URL="redis://localhost:6379"
# Typesense (if needed)
TYPESENSE_URL="http://localhost:8108"
TYPESENSE_API_KEY="bikabika"Data Persistence: All data is stored in the ./.data/ directory, which is already in .gitignore and won't be committed to version control.
Note: Make sure you have Docker and Docker Compose installed. If not, visit Docker's website to download.
Self-Hosted PostgreSQL
# Docker
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=mysecretpassword \
-e POSTGRES_DB=productready \
-p 5432:5432 \
-v postgres_data:/var/lib/postgresql/data \
postgres:16-alpine
# Connection string
PG_DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/productreadyEnvironment Variables
Required
# Database
PG_DATABASE_URL=postgresql://...
# Authentication
BETTER_AUTH_SECRET=your-32-char-random-string
BETTER_AUTH_URL=https://your-production-domain.comOptional - OAuth
# GitHub OAuth
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secretOptional - Email
# Resend
RESEND_API_KEY=re_123456789
# Or Postmark
POSTMARK_API_KEY=your-postmark-token
# Or AWS SES
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/...
AWS_REGION=us-east-1Optional - Payment
# Stripe
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
# Or LemonSqueezy
LEMONSQUEEZY_API_KEY=your-api-key
LEMONSQUEEZY_STORE_ID=your-store-id
LEMONSQUEEZY_WEBHOOK_SECRET=your-webhook-secretGenerate Secrets
# Generate 32-character secret
openssl rand -base64 32
# Or use Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Database Migrations
Production Migrations
Important: Run migrations before deploying new code to avoid errors.
Option 1: Manually
# Install dependencies
pnpm install
# Run migrations
pnpm db:migrateOption 2: CI/CD (recommended)
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'pnpm'
# Run migrations
- run: pnpm install
- run: pnpm db:migrate
env:
PG_DATABASE_URL: ${{ secrets.PG_DATABASE_URL }}
# Deploy (Vercel example)
- run: npx vercel --prod
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}SSL/HTTPS
Vercel
✅ Automatic HTTPS - no configuration needed
Docker/Self-Hosted
Use reverse proxy (nginx or Caddy):
Easiest option - automatic HTTPS:
# Caddyfile
productready.com {
reverse_proxy localhost:3000
}Run:
docker run -d \
-p 80:80 \
-p 443:443 \
-v caddy_data:/data \
-v $PWD/Caddyfile:/etc/caddy/Caddyfile \
caddy:latest# /etc/nginx/sites-available/productready
server {
listen 80;
server_name productready.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name productready.com;
ssl_certificate /etc/letsencrypt/live/productready.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/productready.com/privkey.pem;
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_cache_bypass $http_upgrade;
}
}Get SSL certificate:
# Install certbot
sudo apt install certbot python3-certbot-nginx
# Get certificate
sudo certbot --nginx -d productready.com -d www.productready.comCI/CD Pipeline
GitHub Actions
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'pnpm'
# Run migrations
- run: pnpm install
- run: pnpm db:migrate
env:
PG_DATABASE_URL: ${{ secrets.PG_DATABASE_URL }}
# Deploy to Vercel
- run: npx vercel deploy --prod --token=$VERCEL_TOKEN
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}Health Checks
ProductReady includes a health check endpoint:
GET /api/health
Response:
{
"status": "ok",
"timestamp": "2025-11-19T10:00:00.000Z",
"service": "productready",
"version": "1.0.0",
"buildNumber": "123",
"buildSha": "a1b2c3d"
}Uptime Monitoring
Use health check with monitoring services:
UptimeRobot:
- Add monitor
- URL:
https://productready.com/api/health - Interval: 5 minutes
Pingdom/StatusCake/Better Uptime:
- Similar setup
- Monitor
/api/healthendpoint
Performance Optimization
Enable Edge Runtime (Vercel)
For faster response times:
// src/app/api/*/route.ts
export const runtime = 'edge';Database Connection Pooling
Use connection pooling for better performance:
# Neon (built-in pooling)
PG_DATABASE_URL=postgresql://user:pass@host.neon.tech:5432/db?sslmode=require
# Supabase (transaction mode)
PG_DATABASE_URL=postgresql://postgres.[ref]:[pass]@aws-0-[region].pooler.supabase.com:6543/postgres
# PgBouncer (self-hosted)
docker run -d \
-p 6432:6432 \
-e DATABASE_URL=postgresql://user:pass@postgres:5432/db \
edoburu/pgbouncerCaching
Enable Next.js caching:
// src/app/page.tsx
export const revalidate = 3600; // Revalidate every hour
// Or specific fetch requests
fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
});Monitoring & Logging
Vercel Analytics
Enable in next.config.mjs:
const nextConfig = {
experimental: {
instrumentationHook: true,
},
};Sentry (Error Tracking)
pnpm add @sentry/nextjs// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
});PostHog (Analytics)
pnpm add posthog-js// src/lib/analytics.ts
import posthog from 'posthog-js';
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: 'https://app.posthog.com',
});
}Backup Strategy
Database Backups
Neon:
- Automatic daily backups
- Point-in-time recovery (PITR)
- Configure in dashboard
Supabase:
- Daily backups on paid plans
- Manual backups via Dashboard → Database → Backups
Self-Hosted:
# Backup
pg_dump -h localhost -U postgres productready > backup.sql
# Restore
psql -h localhost -U postgres productready < backup.sql
# Automated (cron)
0 2 * * * pg_dump -h localhost -U postgres productready | gzip > /backups/productready-$(date +\%Y\%m\%d).sql.gzRollback Strategy
Vercel
- Go to Deployments
- Find previous working deployment
- Click "..." → "Promote to Production"
Docker
# Keep previous image
docker tag productready:latest productready:previous
# If new deployment fails, rollback
docker stop productready
docker rm productready
docker run -d --name productready productready:previousPre-Deployment Checklist
Before going to production:
- Set all required environment variables
- Run database migrations
- Test with production database (staging environment)
- Set up custom domain
- Configure SSL/HTTPS
- Set up error tracking (Sentry)
- Set up uptime monitoring
- Set up database backups
- Test OAuth flows (production credentials)
- Test payment webhooks (production mode)
- Update
BETTER_AUTH_URLto production domain - Update OAuth redirect URLs
- Update Stripe/payment webhook URLs
- Configure CSP headers (if needed)
- Enable rate limiting (if needed)
- Set up analytics
Post-Deployment
After deploying:
- Monitor error rates (Sentry)
- Check performance metrics (Vercel Analytics)
- Verify webhooks are working
- Test critical user flows
- Monitor database performance
- Check health endpoint
- Update documentation
- Notify team/users
Troubleshooting
Build Fails
# Check Node.js version
node --version # Should be 18+
# Clear cache and rebuild
rm -rf .next node_modules
pnpm install
pnpm buildDatabase Connection Fails
# Test connection
psql $PG_DATABASE_URL
# Check SSL mode
PG_DATABASE_URL=postgresql://...?sslmode=require
# Verify firewall/security groups allow connectionsWebhooks Not Working
- Check webhook URL is publicly accessible
- Verify webhook secret is correct
- Check provider dashboard for delivery errors
- Use ngrok for testing:
ngrok http 3000
Next Steps
- Deploy to staging - Test everything before production
- Set up monitoring - Track errors and performance
- Configure backups - Protect your data
- Plan scaling - Prepare for growth
Start with Vercel for the easiest deployment experience. Once you're comfortable, explore Docker/Kubernetes for more control.