Next.js Deployment Guide — Vercel, Docker & Self-Hosted in 2026

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

Deploying Next.js correctly is not just about running next build. You need to understand how Server Components, Edge middleware, Image Optimization, and ISR interact with your hosting platform. A misconfigured deployment means features silently break in production — middleware that does not run, images not optimized, or environment variables missing from the server.

This guide covers all major deployment targets: Vercel (the official platform), Docker for self-hosted environments, and standalone Node.js servers.

Deploying to Vercel

Vercel is the first-party platform built by the Next.js team. It has native support for every Next.js feature: edge middleware, ISR, image optimization, server functions, and cron jobs.

Git-Based Deployment

# Push to GitHub/GitLab/Bitbucket
git push origin main

Then connect the repository at vercel.com. Every push to main triggers a production deployment; every pull request gets a preview deployment URL.

Vercel CLI

npm i -g vercel
vercel        # deploy to preview
vercel --prod # deploy to production

Advanced vercel.json Configuration

{
  "buildCommand": "prisma migrate deploy && next build",
  "framework": "nextjs",
  "regions": ["iad1", "sfo1"],
  "functions": {
    "app/api/ai/**/*.ts": {
      "memory": 3008,
      "maxDuration": 120
    },
    "app/api/webhooks/**/*.ts": {
      "memory": 512,
      "maxDuration": 30
    }
  },
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
      ]
    }
  ],
  "crons": [
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 2 * * *"
    }
  ]
}

Environment Variables

Set secrets in the Vercel dashboard under Settings → Environment Variables. For each variable, choose which environments it applies to: Production, Preview, and Development.

# Required for most Next.js + NextAuth apps
AUTH_SECRET=
DATABASE_URL=
NEXTAUTH_URL=https://yourdomain.com
NEXT_PUBLIC_APP_URL=https://yourdomain.com

Prefix client-safe variables with NEXT_PUBLIC_. Server-only variables must never have this prefix.

Vercel Analytics and Speed Insights

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/next'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  )
}

Install the packages:

npm install @vercel/analytics @vercel/speed-insights

Cron Job Route Handler

// app/api/cron/cleanup/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
 
export async function GET(request: NextRequest) {
  const authHeader = request.headers.get('authorization')
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
 
  const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
 
  const { count } = await prisma.session.deleteMany({
    where: { expires: { lt: cutoff } },
  })
 
  return NextResponse.json({ deleted: count, timestamp: new Date().toISOString() })
}

Deploying with Docker

For AWS, GCP, DigitalOcean, or any VPS, Docker is the cleanest deployment method.

Enable the standalone output in next.config.ts:

// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  output: 'standalone',
}
 
export default nextConfig

Multi-stage Dockerfile:

# Dockerfile
FROM node:20-alpine AS base
 
# Install dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci
 
# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
 
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
 
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
 
CMD ["node", "server.js"]

Build and run:

docker build -t my-app .
docker run -p 3000:3000 --env-file .env.production my-app

Docker Compose for Local Production Testing

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - '3000:3000'
    environment:
      - DATABASE_URL=postgresql://postgres:password@db:5432/mydb
      - AUTH_SECRET=${AUTH_SECRET}
    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:

Rollback Strategy

# Vercel
vercel ls                          # list deployments
vercel promote <deployment-url>    # instant rollback to previous deployment
 
# Docker
docker tag my-app:latest my-app:v1.2.3     # tag before deploying
docker run my-app:v1.2.2                   # roll back to previous tag

Pre-Launch Checklist

  • All environment variables set in production (run next build locally with prod env to verify)
  • Database migrations applied (prisma migrate deploy)
  • AUTH_SECRET is a random 32+ character string, not a human-readable value
  • NEXTAUTH_URL matches the exact production domain including https://
  • LCP image has priority prop on next/image
  • robots.txt and sitemap.xml are present and reachable
  • Error pages (not-found.tsx, error.tsx, global-error.tsx) are implemented
  • Security headers are set (X-Frame-Options, X-Content-Type-Options)
  • Rate limiting is applied to /api/auth and other sensitive endpoints
  • Monitoring and alerting configured (Sentry, Datadog, or similar)
  • Backups configured for the database

Common Mistakes

  • Not setting output: 'standalone' before building a Docker image — the image will include node_modules and be hundreds of MB larger
  • Using npm run build without running prisma generate first — Prisma client will be missing in the Docker image
  • Setting NEXT_PUBLIC_ prefix on secrets like DATABASE_URL — this exposes them to the browser
  • Not setting NEXTAUTH_URL in production — NextAuth redirects will use the wrong domain
  • Forgetting to configure connection pooling (?connection_limit=1) for Prisma in serverless environments

Best Practices

  • Use Vercel for the fastest path to production with zero infrastructure management
  • Use Docker + output: 'standalone' for self-hosted deployments to minimize image size
  • Keep secrets in the hosting platform's secret manager, not in .env files committed to git
  • Use preview deployments for every pull request to catch issues before they reach production
  • Configure ISR (revalidate) for content pages to serve cached HTML while revalidating in the background

Key Takeaways

  • Vercel provides native support for every Next.js feature including edge middleware, ISR, and image optimization
  • output: 'standalone' in next.config.ts is required for minimal Docker images
  • Environment variables prefixed with NEXT_PUBLIC_ are bundled into client JavaScript — never use this prefix for secrets
  • vercel promote <url> enables instant rollback to any previous deployment without redeployment
  • Cron jobs on Vercel require authorization header verification using a CRON_SECRET environment variable
  • Run prisma migrate deploy (not migrate dev) in CI/CD pipelines — migrate dev is for local development only
  • ?connection_limit=1 in DATABASE_URL prevents Prisma from overwhelming serverless database connections
  • Security headers (X-Frame-Options, X-Content-Type-Options) should be set in vercel.json or next.config.ts

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading