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/ and static/ 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 .