Deployment cheatsheet.
Vercel (default)
npm i -g vercel
vercel
vercel --prod
Or push to GitHub + import in Vercel dashboard.
Free for hobby, paid for prod. Best Next.js experience but expensive at scale.
Self-host with standalone
// next.config.js
module.exports = {
output: "standalone",
};
npm run build
# Creates .next/standalone/ — includes only deps needed to run
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "server.js"]
docker build -t myapp .
docker run -p 3000:3000 myapp
Dockerfile (multi-stage, optimized)
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
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 HOSTNAME=0.0.0.0
CMD ["node", "server.js"]
ENV vars
# .env.production (committed for non-secrets)
NEXT_PUBLIC_API_URL=https://api.example.com
# .env.local (NEVER commit)
DATABASE_URL=...
AUTH_SECRET=...
NEXT_PUBLIC_* inlined at build → can’t change at runtime (Vercel reload required).
Server-only env vars: server-side only, can be set at runtime.
Runtime env (Vercel)
process.env.DATABASE_URL; // from Vercel dashboard
For Docker: pass at runtime:
docker run -e DATABASE_URL=... -p 3000:3000 myapp
Vercel config (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
],
"redirects": [
{ "source": "/old", "destination": "/new", "permanent": true }
],
"rewrites": [
{ "source": "/api/proxy/:path*", "destination": "https://api.example.com/:path*" }
]
}
Or in next.config.js:
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: [...] }];
},
async redirects() { ... },
async rewrites() { ... },
};
Cron jobs (Vercel)
{
"crons": [
{ "path": "/api/cron/daily", "schedule": "0 0 * * *" }
]
}
Authenticate with CRON_SECRET header:
export async function GET(req: Request) {
if (req.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}
await dailyJob();
return Response.json({ ok: true });
}
ISR on self-host
Standalone output supports ISR. Cache stored in .next/cache/. For multi-instance, use shared storage:
// next.config.js
module.exports = {
experimental: {
isrMemoryCacheSize: 0, // disable in-memory cache for shared external cache
},
};
Cache handler (Redis)
// cache-handler.js
const Redis = require("ioredis");
const redis = new Redis(process.env.REDIS_URL);
class CacheHandler {
async get(key) { /* ... */ }
async set(key, data, ctx) { /* ... */ }
async revalidateTag(tag) { /* ... */ }
}
module.exports = CacheHandler;
// next.config.js
module.exports = {
cacheHandler: require.resolve("./cache-handler.js"),
cacheMaxMemorySize: 0,
};
Image optimization on self-host
Default uses Sharp (heavy). For container deploys, install:
npm i sharp
Or use a CDN (CloudFront, Cloudflare, imgproxy) and configure loader.
Health check
// app/api/health/route.ts
export async function GET() {
await db.$queryRaw`SELECT 1`;
return Response.json({ ok: true });
}
Use in Kubernetes liveness/readiness, Vercel monitoring.
Logging in prod
# Vercel: logs in dashboard, or use logflare/axiom integration
# Self-host: stdout → forward to ELK / Loki
Structured logs:
console.log(JSON.stringify({ level: "info", msg: "...", traceId: "..." }));
Deploy targets
- Vercel: zero config, $$.
- AWS Amplify / SST: pricing similar.
- Cloudflare Pages: smaller features, very cheap.
- Fly.io / Railway: standalone Docker.
- Kubernetes: standalone Docker, full control.
- Static export (
output: "export"): no SSR, just static files.
Static export
// next.config.js
module.exports = { output: "export" };
next build # outputs ./out/
No server features (RSC fetch, server actions, middleware). Pure static. Host on any CDN.
Common mistakes
NEXT_PUBLIC_*for secrets — leaked to client.- Missing standalone copy of
public/andstatic/in Docker. - ISR cache on multi-instance without shared backend.
- Sharp missing on alpine → broken images.
- Forgetting CRON_SECRET — anyone can trigger.
Read this next
If you want my Dockerfile + Vercel templates, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .