260223:1415 20260223 nextJS & nestJS Best pratices
All checks were successful
Build and Deploy / deploy (push) Successful in 4m44s

This commit is contained in:
admin
2026-02-23 14:15:06 +07:00
parent c90a664f53
commit ef16817f38
164 changed files with 24815 additions and 311 deletions

View File

@@ -0,0 +1,371 @@
# Self-Hosting Next.js
Deploy Next.js outside of Vercel with confidence.
## Quick Start: Standalone Output
For Docker or any containerized deployment, use standalone output:
```js
// next.config.js
module.exports = {
output: 'standalone',
};
```
This creates a minimal `standalone` folder with only production dependencies:
```
.next/
├── standalone/
│ ├── server.js # Entry point
│ ├── node_modules/ # Only production deps
│ └── .next/ # Build output
└── static/ # Must be copied separately
```
## Docker Deployment
### Dockerfile
```dockerfile
FROM node:20-alpine AS base
# Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy standalone output
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
```
### Docker Compose
```yaml
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
```
## PM2 Deployment
For traditional server deployments:
```js
// ecosystem.config.js
module.exports = {
apps: [{
name: 'nextjs',
script: '.next/standalone/server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
}],
};
```
```bash
npm run build
pm2 start ecosystem.config.js
```
## ISR and Cache Handlers
### The Problem
ISR (Incremental Static Regeneration) uses filesystem caching by default. This **breaks with multiple instances**:
- Instance A regenerates page → saves to its local disk
- Instance B serves stale page → doesn't see Instance A's cache
- Load balancer sends users to random instances → inconsistent content
### Solution: Custom Cache Handler
Next.js 14+ supports custom cache handlers for shared storage:
```js
// next.config.js
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // Disable in-memory cache
};
```
#### Redis Cache Handler Example
```js
// cache-handler.js
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
const CACHE_PREFIX = 'nextjs:';
module.exports = class CacheHandler {
constructor(options) {
this.options = options;
}
async get(key) {
const data = await redis.get(CACHE_PREFIX + key);
if (!data) return null;
const parsed = JSON.parse(data);
return {
value: parsed.value,
lastModified: parsed.lastModified,
};
}
async set(key, data, ctx) {
const cacheData = {
value: data,
lastModified: Date.now(),
};
// Set TTL based on revalidate option
if (ctx?.revalidate) {
await redis.setex(
CACHE_PREFIX + key,
ctx.revalidate,
JSON.stringify(cacheData)
);
} else {
await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));
}
}
async revalidateTag(tags) {
// Implement tag-based invalidation
// This requires tracking which keys have which tags
}
};
```
#### S3 Cache Handler Example
```js
// cache-handler.js
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({ region: process.env.AWS_REGION });
const BUCKET = process.env.CACHE_BUCKET;
module.exports = class CacheHandler {
async get(key) {
try {
const response = await s3.send(new GetObjectCommand({
Bucket: BUCKET,
Key: `cache/${key}`,
}));
const body = await response.Body.transformToString();
return JSON.parse(body);
} catch (err) {
if (err.name === 'NoSuchKey') return null;
throw err;
}
}
async set(key, data, ctx) {
await s3.send(new PutObjectCommand({
Bucket: BUCKET,
Key: `cache/${key}`,
Body: JSON.stringify({
value: data,
lastModified: Date.now(),
}),
ContentType: 'application/json',
}));
}
};
```
## What Works vs What Needs Setup
| Feature | Single Instance | Multi-Instance | Notes |
|---------|----------------|----------------|-------|
| SSR | Yes | Yes | No special setup |
| SSG | Yes | Yes | Built at deploy time |
| ISR | Yes | Needs cache handler | Filesystem cache breaks |
| Image Optimization | Yes | Yes | CPU-intensive, consider CDN |
| Middleware | Yes | Yes | Runs on Node.js |
| Edge Runtime | Limited | Limited | Some features Node-only |
| `revalidatePath/Tag` | Yes | Needs cache handler | Must share cache |
| `next/font` | Yes | Yes | Fonts bundled at build |
| Draft Mode | Yes | Yes | Cookie-based |
## Image Optimization
Next.js Image Optimization works out of the box but is CPU-intensive.
### Option 1: Built-in (Simple)
Works automatically, but consider:
- Set `deviceSizes` and `imageSizes` in config to limit variants
- Use `minimumCacheTTL` to reduce regeneration
```js
// next.config.js
module.exports = {
images: {
minimumCacheTTL: 60 * 60 * 24, // 24 hours
deviceSizes: [640, 750, 1080, 1920], // Limit sizes
},
};
```
### Option 2: External Loader (Recommended for Scale)
Offload to Cloudinary, Imgix, or similar:
```js
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './lib/image-loader.js',
},
};
```
```js
// lib/image-loader.js
export default function cloudinaryLoader({ src, width, quality }) {
const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];
return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;
}
```
## Environment Variables
### Build-time vs Runtime
```js
// Available at build time only (baked into bundle)
NEXT_PUBLIC_API_URL=https://api.example.com
// Available at runtime (server-side only)
DATABASE_URL=postgresql://...
API_SECRET=...
```
### Runtime Configuration
For truly dynamic config, don't use `NEXT_PUBLIC_*`. Instead:
```tsx
// app/api/config/route.ts
export async function GET() {
return Response.json({
apiUrl: process.env.API_URL,
features: process.env.FEATURES?.split(','),
});
}
```
## OpenNext: Serverless Without Vercel
[OpenNext](https://open-next.js.org/) adapts Next.js for AWS Lambda, Cloudflare Workers, etc.
```bash
npx create-sst@latest
# or
npx @opennextjs/aws build
```
Supports:
- AWS Lambda + CloudFront
- Cloudflare Workers
- Netlify Functions
- Deno Deploy
## Health Check Endpoint
Always include a health check for load balancers:
```tsx
// app/api/health/route.ts
export async function GET() {
try {
// Optional: check database connection
// await db.$queryRaw`SELECT 1`;
return Response.json({ status: 'healthy' }, { status: 200 });
} catch (error) {
return Response.json({ status: 'unhealthy' }, { status: 503 });
}
}
```
## Pre-Deployment Checklist
1. **Build locally first**: `npm run build` - catch errors before deploy
2. **Test standalone output**: `node .next/standalone/server.js`
3. **Set `output: 'standalone'`** for Docker
4. **Configure cache handler** for multi-instance ISR
5. **Set `HOSTNAME="0.0.0.0"`** for containers
6. **Copy `public/` and `.next/static/`** - not included in standalone
7. **Add health check endpoint**
8. **Test ISR revalidation** after deployment
9. **Monitor memory usage** - Node.js defaults may need tuning
## Testing Cache Handler
**Critical**: Test your cache handler on every Next.js upgrade:
```bash
# Start multiple instances
PORT=3001 node .next/standalone/server.js &
PORT=3002 node .next/standalone/server.js &
# Trigger ISR revalidation
curl http://localhost:3001/api/revalidate?path=/posts
# Verify both instances see the update
curl http://localhost:3001/posts
curl http://localhost:3002/posts
# Should return identical content
```