bookworm-smart-assistant/skills/nextjs-developer/references/deployment.md

546 lines
11 KiB
Markdown

# Deployment & Production
## Vercel Deployment (Recommended)
### Quick Deploy
```bash
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Production deployment
vercel --prod
```
### vercel.json Configuration
```json
{
"buildCommand": "next build",
"devCommand": "next dev",
"installCommand": "npm install",
"framework": "nextjs",
"regions": ["iad1"],
"env": {
"DATABASE_URL": "@database-url",
"NEXT_PUBLIC_API_URL": "https://api.example.com"
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE" }
]
}
],
"redirects": [
{
"source": "/old-blog/:slug",
"destination": "/blog/:slug",
"permanent": true
}
],
"rewrites": [
{
"source": "/api/:path*",
"destination": "https://api.example.com/:path*"
}
]
}
```
### Environment Variables
```bash
# .env.local (not committed)
DATABASE_URL="postgresql://user:pass@localhost:5432/db"
NEXTAUTH_SECRET="your-secret"
# .env.production (committed, public vars only)
NEXT_PUBLIC_API_URL="https://api.example.com"
```
```tsx
// Access in Server Components
const dbUrl = process.env.DATABASE_URL
// Access in Client Components (must be prefixed with NEXT_PUBLIC_)
const apiUrl = process.env.NEXT_PUBLIC_API_URL
```
## Self-Hosting
### Standalone Output
```js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfig
```
```bash
# Build
npm run build
# The standalone folder contains everything needed
# Copy these to your server:
# - .next/standalone/
# - .next/static/
# - public/
# Run on server
node .next/standalone/server.js
```
### Node.js Server
```bash
# Build
npm run build
# Start production server
npm start
# With PM2 for process management
pm2 start npm --name "nextjs" -- start
pm2 startup
pm2 save
```
## Docker Deployment
### Dockerfile (Multi-stage)
```dockerfile
# Stage 1: Dependencies
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Stage 2: Builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Stage 3: 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 --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"]
```
### docker-compose.yml
```yaml
version: '3.8'
services:
nextjs:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
- NEXTAUTH_URL=http://localhost:3000
- NEXTAUTH_SECRET=your-secret
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
```
```bash
# Build and run
docker-compose up -d
# View logs
docker-compose logs -f nextjs
# Rebuild
docker-compose up -d --build
```
## Production Optimization
### next.config.js
```js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Standalone for self-hosting
output: 'standalone',
// Image optimization
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
// Compression
compress: true,
// Security headers
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin'
},
],
},
]
},
// Experimental features
experimental: {
optimizePackageImports: ['@mui/material', 'lodash'],
},
// Bundle analyzer
webpack: (config, { isServer }) => {
if (process.env.ANALYZE === 'true') {
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: isServer
? '../analyze/server.html'
: './analyze/client.html',
})
)
}
return config
},
}
module.exports = nextConfig
```
### Bundle Analysis
```bash
# Install analyzer
npm install -D @next/bundle-analyzer
# Analyze
ANALYZE=true npm run build
# Or use built-in
npm run build -- --experimental-build-mode=compile
```
### Performance Monitoring
```tsx
// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next'
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
{children}
<SpeedInsights />
<Analytics />
</body>
</html>
)
}
```
## CDN & Edge
### Static Asset CDN
```js
// next.config.js
const nextConfig = {
assetPrefix: process.env.NODE_ENV === 'production'
? 'https://cdn.example.com'
: '',
}
```
### Edge Runtime
```tsx
// app/api/edge/route.ts
export const runtime = 'edge'
export async function GET(request: Request) {
return new Response('Hello from Edge!', {
status: 200,
headers: {
'content-type': 'text/plain',
},
})
}
// app/page.tsx
export const runtime = 'edge'
export default async function Page() {
return <div>Edge-rendered page</div>
}
```
## Caching Strategy
### ISR (Incremental Static Regeneration)
```tsx
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // Revalidate every hour
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetchPost(params.slug)
return <article>{post.content}</article>
}
```
### On-Demand Revalidation
```tsx
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ message: 'Invalid secret' }, { status: 401 })
}
const path = request.nextUrl.searchParams.get('path') || '/'
revalidatePath(path)
return Response.json({ revalidated: true, now: Date.now() })
}
```
## Database Connection Pooling
```ts
// lib/db.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = global as unknown as {
prisma: PrismaClient | undefined
}
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
```
## Health Check Endpoint
```tsx
// app/api/health/route.ts
import { db } from '@/lib/db'
export async function GET() {
try {
// Check database connection
await db.$queryRaw`SELECT 1`
return Response.json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
})
} catch (error) {
return Response.json(
{
status: 'error',
message: 'Database connection failed',
},
{ status: 503 }
)
}
}
```
## CI/CD with GitHub Actions
```yaml
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'
```
## Monitoring & Logging
```tsx
// app/error.tsx
'use client'
import * as Sentry from '@sentry/nextjs'
import { useEffect } from 'react'
export default function Error({
error,
}: {
error: Error & { digest?: string }
}) {
useEffect(() => {
Sentry.captureException(error)
}, [error])
return <div>Something went wrong!</div>
}
```
## Quick Reference
| Platform | Best For | Effort |
|----------|----------|--------|
| **Vercel** | Zero-config, optimal performance | Low |
| **Netlify** | Alternative to Vercel | Low |
| **Railway** | Simple hosting with databases | Medium |
| **AWS/GCP** | Enterprise, custom needs | High |
| **Docker** | Self-hosting, full control | High |
## Production Checklist
- [ ] Enable TypeScript strict mode
- [ ] Configure CSP headers
- [ ] Setup error monitoring (Sentry)
- [ ] Configure analytics (Vercel/GA)
- [ ] Optimize images (next/image)
- [ ] Enable compression
- [ ] Setup CDN for static assets
- [ ] Configure database connection pooling
- [ ] Add health check endpoint
- [ ] Setup CI/CD pipeline
- [ ] Configure environment variables
- [ ] Enable ISR/SSG where possible
- [ ] Test Core Web Vitals
- [ ] Setup logging (Datadog/LogRocket)
- [ ] Configure backup strategy