All guides
Engineering12 min read

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.

NC

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/image optimization runs on Vercel's infrastructure
  • Analytics: Core Web Vitals tracking with no extra configuration
  • Cron jobs: Scheduled functions via vercel.json

Deployment Steps

code
# 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:

code
# 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

code
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:

code
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

code
# 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.

code
// next.config.ts — required for Cloudflare Pages
const nextConfig = {
  // All pages must be edge-compatible
};
code
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

code
// 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:

code
# 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

code
// 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

code
// 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',
});
code
// 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:

code
// 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

WhatToolWhy
Error trackingSentryCatch and debug runtime errors
PerformanceVercel Analytics / DatadogIdentify slow pages
UptimeBetter Uptime / PingdomKnow when the site is down
LogsAxiom / DatadogDebug 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:

code
# 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.

Insights once a month. No spam. Unsubscribe anytime.