Next.js Deployment Guide: From Development to Production
Everything you need to know to deploy Next.js applications reliably — covering Vercel, self-hosted options, environment configuration, database setup, and production readiness.
Nextcraft Agency
The Deployment Decision
Next.js runs well on several platforms. Choosing the wrong one for your use case adds complexity and operational overhead. This guide covers the practical tradeoffs.
Option 1: Vercel (Recommended for Most)
Vercel built Next.js and is the deployment platform with the most native integration. Most Next.js features work without configuration.
What Vercel Handles Automatically
- CDN and edge network: Static assets on a global CDN, edge functions in 80+ regions
- Preview deployments: Every git push gets a unique preview URL
- Environment variables: Per-environment variables with preview/production separation
- Serverless scaling: Zero-config scaling for both Server Components and API routes
- Image optimization:
next/imageoptimization runs on Vercel's infrastructure - Analytics: Core Web Vitals tracking with no extra configuration
- Cron jobs: Scheduled functions via
vercel.json
Deployment Steps
# Install Vercel CLI
npm i -g vercel
# Deploy from your project directory
vercel
# Follow prompts to link to a project
# First deployment creates a preview URL
# Production deployment: vercel --prod
Or connect your GitHub repo in the Vercel dashboard — every push to main deploys automatically.
Environment Variables
In Vercel Dashboard → Settings → Environment Variables:
# Production only
DATABASE_URL=postgresql://...
STRIPE_SECRET_KEY=sk_live_...
NEXTAUTH_SECRET=your-secret
# Preview and development
DATABASE_URL=postgresql://...preview
STRIPE_SECRET_KEY=sk_test_...
NEXTAUTH_SECRET=dev-secret
# All environments (public variables)
NEXT_PUBLIC_APP_URL=https://yourapp.com
Variables prefixed with NEXT_PUBLIC_ are exposed to the browser. Never put secrets in NEXT_PUBLIC_ variables.
Cost Considerations
Vercel's pricing is based on:
- Bandwidth: Data transferred from CDN
- Function invocations: Each Server Component render or API call
- Build minutes: Time spent building
For most SaaS applications under 50k monthly active users, Vercel's Pro plan ($20/month) is sufficient. High-traffic applications with many dynamic routes can get expensive — evaluate at scale.
Option 2: Self-Hosted with Docker
For teams with specific compliance requirements, cost sensitivity at scale, or preference for infrastructure control.
The Dockerfile
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Production image — minimal size
FROM base 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"]
Enable standalone output in next.config.ts:
const nextConfig = {
output: 'standalone',
};
The standalone output bundles only the files required to run the server — typical output is 50–150MB vs hundreds of MB for a full node_modules.
Docker Compose for Local Development
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- '3000:3000'
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/mydb
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- NEXTAUTH_URL=http://localhost:3000
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
Running on VPS (Coolify, Render, Railway)
Managed platforms that accept Docker containers are the easiest self-hosted option:
- Coolify: Self-hosted PaaS. Runs on your own VPS. Free software.
- Render: Managed Docker hosting. Simple pricing, good DX.
- Railway: Managed hosting with built-in databases. Excellent for small to medium SaaS.
- Fly.io: Distributed compute with edge-close deployment.
These give you more control than Vercel with less operational overhead than bare Kubernetes.
Option 3: Cloudflare Pages
For applications that can run entirely on the edge runtime — no Node.js APIs, no direct database connections.
// next.config.ts — required for Cloudflare Pages
const nextConfig = {
// All pages must be edge-compatible
};
npx @cloudflare/next-on-pages@1
Best for: static-heavy sites, global performance priority, teams already in the Cloudflare ecosystem using KV/D1/R2.
Production Readiness Checklist
Before going live, verify these:
Environment Configuration
// lib/env.ts — validate at startup, not at runtime
import { z } from 'zod';
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXT_PUBLIC_APP_URL: z.string().url(),
// Add all required env vars
});
export const env = EnvSchema.parse(process.env);
If a required variable is missing, the app crashes on startup with a clear message — better than discovering it via a runtime error in production.
Database Migrations
Never run prisma migrate dev in production. Use prisma migrate deploy:
# In your deployment script, before starting the server
npx prisma migrate deploy
This applies only pending migrations, without modifying or deleting existing migrations.
Security Headers
// next.config.ts
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
],
},
];
},
};
Test your security headers at securityheaders.com.
Error Reporting
// Sentry in next.config.ts (using @sentry/nextjs)
import { withSentryConfig } from '@sentry/nextjs';
const nextConfig = { /* ... */ };
export default withSentryConfig(nextConfig, {
silent: true,
org: 'your-org',
project: 'your-project',
});
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1, // 10% of requests
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
});
Health Check Endpoint
Add a health check endpoint for load balancers and monitoring:
// app/api/health/route.ts
export async function GET() {
try {
// Verify DB connection
await db.$queryRaw`SELECT 1`;
return Response.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
} catch (error) {
return Response.json(
{ status: 'error', message: 'Database unavailable' },
{ status: 503 }
);
}
}
Monitoring in Production
The Minimum Monitoring Stack
| What | Tool | Why |
|---|---|---|
| Error tracking | Sentry | Catch and debug runtime errors |
| Performance | Vercel Analytics / Datadog | Identify slow pages |
| Uptime | Better Uptime / Pingdom | Know when the site is down |
| Logs | Axiom / Datadog | Debug issues after they happen |
Alerting
Configure alerts for:
- Error rate spike (Sentry: more than X errors/hour)
- Uptime check failure (Better Uptime: site down for 1+ minutes)
- Database connection failures
- Disk usage above 80% (for self-hosted)
Alerts without on-call processes don't help. Define who gets alerted and what they do.
Zero-Downtime Deployment
Vercel handles zero-downtime deployment automatically — new deployments are atomic, traffic is switched to the new version only when it's healthy.
For self-hosted:
# Docker Compose with rolling update
services:
app:
image: myapp:${VERSION}
deploy:
update_config:
parallelism: 1
delay: 10s
order: start-first # Start new instance before stopping old
rollback_config:
parallelism: 1
order: stop-first
Blue-green deployment is even safer: bring up the new environment completely, test it, then switch traffic. Keep the old environment ready to switch back for 30 minutes.
Stay Informed.
Join 1,200+ founders and engineers receiving our monthly deep dives on product engineering, design, and growth.