Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Apply Next.js best practices for RSC boundaries, async APIs, routing, metadata, and optimization.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
self-hosting.md
1# Self-Hosting Next.js23Deploy Next.js outside of Vercel with confidence.45## Quick Start: Standalone Output67For Docker or any containerized deployment, use standalone output:89```js10// next.config.js11module.exports = {12output: 'standalone',13};14```1516This creates a minimal `standalone` folder with only production dependencies:1718```19.next/20├── standalone/21│ ├── server.js # Entry point22│ ├── node_modules/ # Only production deps23│ └── .next/ # Build output24└── static/ # Must be copied separately25```2627## Docker Deployment2829### Dockerfile3031```dockerfile32FROM node:20-alpine AS base3334# Install dependencies35FROM base AS deps36WORKDIR /app37COPY package.json package-lock.json* ./38RUN npm ci3940# Build41FROM base AS builder42WORKDIR /app43COPY --from=deps /app/node_modules ./node_modules44COPY . .45RUN npm run build4647# Production48FROM base AS runner49WORKDIR /app5051ENV NODE_ENV=production5253# Create non-root user54RUN addgroup --system --gid 1001 nodejs55RUN adduser --system --uid 1001 nextjs5657# Copy standalone output58COPY --from=builder /app/.next/standalone ./59COPY --from=builder /app/.next/static ./.next/static60COPY --from=builder /app/public ./public6162USER nextjs6364EXPOSE 300065ENV PORT=300066ENV HOSTNAME="0.0.0.0"6768CMD ["node", "server.js"]69```7071### Docker Compose7273```yaml74version: '3.8'7576services:77web:78build: .79ports:80- "3000:3000"81environment:82- NODE_ENV=production83restart: unless-stopped84healthcheck:85test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]86interval: 30s87timeout: 10s88retries: 389```9091## PM2 Deployment9293For traditional server deployments:9495```js96// ecosystem.config.js97module.exports = {98apps: [{99name: 'nextjs',100script: '.next/standalone/server.js',101instances: 'max',102exec_mode: 'cluster',103env: {104NODE_ENV: 'production',105PORT: 3000,106},107}],108};109```110111```bash112npm run build113pm2 start ecosystem.config.js114```115116## ISR and Cache Handlers117118### The Problem119120ISR (Incremental Static Regeneration) uses filesystem caching by default. This **breaks with multiple instances**:121122- Instance A regenerates page → saves to its local disk123- Instance B serves stale page → doesn't see Instance A's cache124- Load balancer sends users to random instances → inconsistent content125126### Solution: Custom Cache Handler127128Next.js 14+ supports custom cache handlers for shared storage:129130```js131// next.config.js132module.exports = {133cacheHandler: require.resolve('./cache-handler.js'),134cacheMaxMemorySize: 0, // Disable in-memory cache135};136```137138#### Redis Cache Handler Example139140```js141// cache-handler.js142const Redis = require('ioredis');143144const redis = new Redis(process.env.REDIS_URL);145const CACHE_PREFIX = 'nextjs:';146147module.exports = class CacheHandler {148constructor(options) {149this.options = options;150}151152async get(key) {153const data = await redis.get(CACHE_PREFIX + key);154if (!data) return null;155156const parsed = JSON.parse(data);157return {158value: parsed.value,159lastModified: parsed.lastModified,160};161}162163async set(key, data, ctx) {164const cacheData = {165value: data,166lastModified: Date.now(),167};168169// Set TTL based on revalidate option170if (ctx?.revalidate) {171await redis.setex(172CACHE_PREFIX + key,173ctx.revalidate,174JSON.stringify(cacheData)175);176} else {177await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));178}179}180181async revalidateTag(tags) {182// Implement tag-based invalidation183// This requires tracking which keys have which tags184}185};186```187188#### S3 Cache Handler Example189190```js191// cache-handler.js192const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');193194const s3 = new S3Client({ region: process.env.AWS_REGION });195const BUCKET = process.env.CACHE_BUCKET;196197module.exports = class CacheHandler {198async get(key) {199try {200const response = await s3.send(new GetObjectCommand({201Bucket: BUCKET,202Key: `cache/${key}`,203}));204const body = await response.Body.transformToString();205return JSON.parse(body);206} catch (err) {207if (err.name === 'NoSuchKey') return null;208throw err;209}210}211212async set(key, data, ctx) {213await s3.send(new PutObjectCommand({214Bucket: BUCKET,215Key: `cache/${key}`,216Body: JSON.stringify({217value: data,218lastModified: Date.now(),219}),220ContentType: 'application/json',221}));222}223};224```225226## What Works vs What Needs Setup227228| Feature | Single Instance | Multi-Instance | Notes |229|---------|----------------|----------------|-------|230| SSR | Yes | Yes | No special setup |231| SSG | Yes | Yes | Built at deploy time |232| ISR | Yes | Needs cache handler | Filesystem cache breaks |233| Image Optimization | Yes | Yes | CPU-intensive, consider CDN |234| Middleware | Yes | Yes | Runs on Node.js |235| Edge Runtime | Limited | Limited | Some features Node-only |236| `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache |237| `next/font` | Yes | Yes | Fonts bundled at build |238| Draft Mode | Yes | Yes | Cookie-based |239240## Image Optimization241242Next.js Image Optimization works out of the box but is CPU-intensive.243244### Option 1: Built-in (Simple)245246Works automatically, but consider:247- Set `deviceSizes` and `imageSizes` in config to limit variants248- Use `minimumCacheTTL` to reduce regeneration249250```js251// next.config.js252module.exports = {253images: {254minimumCacheTTL: 60 * 60 * 24, // 24 hours255deviceSizes: [640, 750, 1080, 1920], // Limit sizes256},257};258```259260### Option 2: External Loader (Recommended for Scale)261262Offload to Cloudinary, Imgix, or similar:263264```js265// next.config.js266module.exports = {267images: {268loader: 'custom',269loaderFile: './lib/image-loader.js',270},271};272```273274```js275// lib/image-loader.js276export default function cloudinaryLoader({ src, width, quality }) {277const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];278return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;279}280```281282## Environment Variables283284### Build-time vs Runtime285286```js287// Available at build time only (baked into bundle)288NEXT_PUBLIC_API_URL=https://api.example.com289290// Available at runtime (server-side only)291DATABASE_URL=postgresql://...292API_SECRET=...293```294295### Runtime Configuration296297For truly dynamic config, don't use `NEXT_PUBLIC_*`. Instead:298299```tsx300// app/api/config/route.ts301export async function GET() {302return Response.json({303apiUrl: process.env.API_URL,304features: process.env.FEATURES?.split(','),305});306}307```308309## OpenNext: Serverless Without Vercel310311[OpenNext](https://open-next.js.org/) adapts Next.js for AWS Lambda, Cloudflare Workers, etc.312313```bash314npx create-sst@latest315# or316npx @opennextjs/aws build317```318319Supports:320- AWS Lambda + CloudFront321- Cloudflare Workers322- Netlify Functions323- Deno Deploy324325## Health Check Endpoint326327Always include a health check for load balancers:328329```tsx330// app/api/health/route.ts331export async function GET() {332try {333// Optional: check database connection334// await db.$queryRaw`SELECT 1`;335336return Response.json({ status: 'healthy' }, { status: 200 });337} catch (error) {338return Response.json({ status: 'unhealthy' }, { status: 503 });339}340}341```342343## Pre-Deployment Checklist3443451. **Build locally first**: `npm run build` - catch errors before deploy3462. **Test standalone output**: `node .next/standalone/server.js`3473. **Set `output: 'standalone'`** for Docker3484. **Configure cache handler** for multi-instance ISR3495. **Set `HOSTNAME="0.0.0.0"`** for containers3506. **Copy `public/` and `.next/static/`** - not included in standalone3517. **Add health check endpoint**3528. **Test ISR revalidation** after deployment3539. **Monitor memory usage** - Node.js defaults may need tuning354355## Testing Cache Handler356357**Critical**: Test your cache handler on every Next.js upgrade:358359```bash360# Start multiple instances361PORT=3001 node .next/standalone/server.js &362PORT=3002 node .next/standalone/server.js &363364# Trigger ISR revalidation365curl http://localhost:3001/api/revalidate?path=/posts366367# Verify both instances see the update368curl http://localhost:3001/posts369curl http://localhost:3002/posts370# Should return identical content371```372