This guide provides comprehensive instructions for deploying the NextJS starter template with Better Auth v1.4+ and Drizzle ORM v0.45+ to various hosting platforms, including production, staging, and development environments.
┌─────────────────────────────────────────────────────────────┐
│ Deployment Environments │
├─────────────────────────────────────────────────────────────┤
│ Development │
│ ├── Local development │
│ ├── Feature branches │
│ └── Pull request previews │
├─────────────────────────────────────────────────────────────┤
│ Staging │
│ ├── Integration testing │
│ ├── UAT (User Acceptance Testing) │
│ └── Performance testing │
├─────────────────────────────────────────────────────────────┤
│ Production │
│ ├── Live application │
│ ├── High availability │
│ └── Scalable infrastructure │
└─────────────────────────────────────────────────────────────┘- Recommended: Vercel (native Next.js support)
- Alternative: Netlify, AWS, DigitalOcean, Railway
- Self-hosted: Docker containers on cloud providers
# Application
NODE_ENV="production"
NEXT_PUBLIC_APP_URL="https://yourdomain.com"
NEXTAUTH_URL="https://yourdomain.com"
NEXTAUTH_SECRET="your-production-secret-key"
# Database (Use managed database service)
DATABASE_URL="postgresql://user:pass@host:5432/dbname"
DB_SSL=true
DB_HOST="your-db-host.rds.amazonaws.com"
DB_PORT=5432
DB_NAME="production_db"
DB_USER="db_user"
DB_PASSWORD="secure_password"
# Authentication
BETTER_AUTH_SECRET="your-production-better-auth-secret"
BETTER_AUTH_URL="https://yourdomain.com"
# External Services
GOOGLE_CLIENT_ID="your-google-oauth-client-id"
GOOGLE_CLIENT_SECRET="your-google-oauth-client-secret"
RESEND_API_KEY="your-production-resend-key"
RESEND_FROM_EMAIL="noreply@yourdomain.com"
# Analytics (Optional)
GOOGLE_ANALYTICS_ID="G-XXXXXXXXXX"# Application
NODE_ENV="production"
NEXT_PUBLIC_APP_URL="https://staging.yourdomain.com"
NEXTAUTH_URL="https://staging.yourdomain.com"
# Database (Staging database)
DATABASE_URL="postgresql://user:pass@staging-db-host:5432/staging_db"
# Authentication (Staging secrets)
BETTER_AUTH_SECRET="staging-secret-key"
NEXTAUTH_SECRET="staging-nextauth-secret"
# Email (Staging email service)
RESEND_API_KEY="staging-resend-key"
RESEND_FROM_EMAIL="staging@yourdomain.com"npm i -g vercel# Link project to Vercel
vercel link
# Deploy for first time
vercel --prodvercel.json)1. Go to Vercel Dashboard
2. Select your project
3. Go to Settings → Environment Variables
4. Add all required variables
# Add environment variables
vercel env add DATABASE_URL
vercel env add NEXTAUTH_SECRET
vercel env add BETTER_AUTH_SECRET
vercel env add GOOGLE_CLIENT_ID
vercel env add GOOGLE_CLIENT_SECRET
vercel env add RESEND_API_KEY
# Choose environment scope (production, preview, development)# Install Vercel Postgres CLI
npm i @vercel/postgres
# Create database
vercel postgres create
# Link to project
vercel postgres link
# Run migrations
vercel env pull .env.local
npm run db:migrate# Set up connection pooling
# Example with Supabase or PlanetScale
DATABASE_URL="postgresql://user:[YOUR-PASSWORD]@db.your-project.supabase.co:5432/postgres"1. Go to Project Settings → Domains
2. Add your custom domain
3. Configure DNS records as instructed
4. Wait for SSL certificate issuance
# Vercel automatically handles DNS and SSL
# Just add your domain and update nameservers:
# ns1.vercel-dns.com
# ns2.vercel-dns-d1.com# Connect GitHub repository
# Vercel will automatically deploy on push to main branch
# Enable preview deployments for pull requests
# Settings → Git → Preview Deployments# Deploy to production
vercel --prod
# Deploy to preview
vercel
# Deploy specific commit
vercel --prod --sha <commit-hash>version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@db:5432/nextjs_starter
- NEXTAUTH_SECRET=your-secret
- BETTER_AUTH_SECRET=your-better-auth-secret
depends_on:
- db
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
- POSTGRES_DB=nextjs_starter
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
restart: unless-stopped
volumes:
postgres_data:version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- RESEND_API_KEY=${RESEND_API_KEY}
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/ssl/certs
depends_on:
- app
restart: unless-stopped
volumes:
postgres_data:nginx.confevents {
worker_connections 1024;
}
http {
upstream app {
server app:3000;
}
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/ssl/certs/yourdomain.com.crt;
ssl_certificate_key /etc/ssl/certs/yourdomain.com.key;
location / {
proxy_pass http://app;
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;
}
location /api {
proxy_pass http://app;
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_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
}# task-definition.json
{
"family": "nextjs-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::account:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::account:role/ecsTaskRole",
"containerDefinitions":
[
{
"name": "nextjs-app",
"image": "your-account.dkr.ecr.region.amazonaws.com/nextjs-app:latest",
"portMappings": [{ "containerPort": 3000, "protocol": "tcp" }],
"environment": [{ "name": "NODE_ENV", "value": "production" }],
"secrets":
[{ "name": "DATABASE_URL", "valueFrom": "arn:aws:secretsmanager:region:account:secret:database-url" }],
"logConfiguration":
{
"logDriver": "awslogs",
"options":
{ "awslogs-group": "/ecs/nextjs-app", "awslogs-region": "us-east-1", "awslogs-stream-prefix": "ecs" },
},
},
],
}# Create RDS instance via AWS CLI
aws rds create-db-instance \
--db-instance-identifier nextjs-starter-db \
--db-instance-class db.t3.micro \
--engine postgres \
--master-username postgres \
--master-user-password your-secure-password \
--allocated-storage 20 \
--vpc-security-group-ids sg-xxxxxxxxx \
--db-subnet-group-name default \
--backup-retention-period 7 \
--storage-encryptedapp.yamlname: nextjs-starter
services:
- name: web
source_dir: /
github:
repo: your-username/nextjs-starter-better-drizzle
branch: main
run_command: npm start
environment_slug: node-js
instance_count: 1
instance_size_slug: basic-xxs
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
value: ${db.DATABASE_URL}
- key: NEXTAUTH_SECRET
value: ${NEXTAUTH_SECRET}
- key: BETTER_AUTH_SECRET
value: ${BETTER_AUTH_SECRET}
databases:
- name: db
engine: PG
version: "15"# Install Railway CLI
npm install -g @railway/cli
# Login to Railway
railway login
# Initialize project
railway init
# Add database
railway add postgresql
# Set environment variables
railway variables set NODE_ENV=production
railway variables set NEXTAUTH_SECRET=your-secret
# Deploy
railway up.github/workflows/deploy.ymlname: Deploy to Production
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
- name: Install dependencies
run: npm install --force
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run tests
run: npm run test:coverage
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
NEXTAUTH_SECRET: test-secret
BETTER_AUTH_SECRET: test-better-auth-secret
- name: Upload coverage reports
uses: codecov/codecov-action@v3
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
- name: Install dependencies
run: npm install --force
- name: Build application
run: npm run build
env:
NEXT_PUBLIC_APP_URL: https://yourdomain.com
NEXTAUTH_URL: https://yourdomain.com
- name: Build Docker image
run: docker build -t nextjs-starter .
- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: |
# Deploy to your platform of choice
# Example: Vercel, AWS, DigitalOcean
echo "Deploying to production..."// vercel.json
{
"git": {
"deploymentEnabled": {
"main": true,
"dev": false
}
},
"build": {
"env": {
"NEXT_PUBLIC_APP_URL": "https://yourdomain.com"
}
}
}// pages/_app.tsx
import { Analytics } from '@vercel/analytics/react';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Analytics />
</>
);
}// src/lib/error-tracking.ts
export function trackError(error: Error, context?: Record<string, any>) {
// Send to error tracking service
console.error("Application Error:", error, context);
// Send to external service (Sentry, LogRocket, etc.)
if (typeof window !== "undefined") {
// Client-side error tracking
}
}// src/app/api/health/route.ts
import { NextResponse } from "next/server";
import { db } from "@/db";
export async function GET() {
try {
// Check database connection
await db.select().from("users").limit(1);
return NextResponse.json({
status: "healthy",
timestamp: new Date().toISOString(),
version: process.env.npm_package_version,
environment: process.env.NODE_ENV,
});
} catch (error) {
return NextResponse.json(
{
status: "unhealthy",
error: error.message,
timestamp: new Date().toISOString(),
},
{ status: 500 },
);
}
}// src/lib/logger.ts
import winston from "winston";
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || "info",
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
),
transports: [
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.File({ filename: "combined.log" }),
],
});
if (process.env.NODE_ENV !== "production") {
logger.add(
new winston.transports.Console({
format: winston.format.simple(),
}),
);
}// src/db/index.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
const connectionString = process.env.DATABASE_URL;
const client = postgres(connectionString, {
max: 10, // Maximum number of connections
idle_timeout: 20,
connect_timeout: 10,
ssl: process.env.DB_SSL === "true" ? "require" : false,
});
export const db = drizzle(client);- [ ] Use strong, unique secrets
- [ ] Enable SSL/TLS for all connections
- [ ] Use managed database services
- [ ] Regularly rotate secrets
- [ ] Enable audit logging
- [ ] Content Security Policy (CSP)
- [ ] XSS protection headers
- [ ] CSRF protection
- [ ] Rate limiting
- [ ] Input validation
- [ ] Firewall configuration
- [ ] VPN access for admin
- [ ] Regular security updates
- [ ] Backup encryption
- [ ] Disaster recovery plan
# Backup user uploads
rsync -av /app/uploads/ /backups/uploads/$(date +\%Y\%m\%d)/# Check database connectivity
psql $DATABASE_URL -c "SELECT 1;"
# Check connection pool status
SELECT * FROM pg_stat_activity WHERE datname = 'your_db_name';# Monitor memory usage
docker stats
# Increase Node.js memory limit
export NODE_OPTIONS="--max-old-space-size=4096"# Clear build cache
rm -rf .next
npm run build
# Check disk space
df -h# Check SSL certificate
openssl s_client -connect yourdomain.com:443
# Renew SSL certificate (if using certbot)
certbot renew// Middleware for performance monitoring
export function middleware(request: NextRequest) {
const start = Date.now();
const response = NextResponse.next();
const duration = Date.now() - start;
console.log(`${request.method} ${request.url} - ${duration}ms`);
return response;
}-- Slow query logging
SELECT query, mean_time, calls, total_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;---
Document Version: 1.1
Last Updated: December 8, 2024
Next Review: January 31, 2025