Next.js Deployment Guide — Vercel, Docker & Self-Hosted in 2026
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 mainThen 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 productionAdvanced 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.comPrefix 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-insightsCron 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 nextConfigMulti-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-appDocker 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 tagPre-Launch Checklist
- All environment variables set in production (run
next buildlocally with prod env to verify) - Database migrations applied (
prisma migrate deploy) -
AUTH_SECRETis a random 32+ character string, not a human-readable value -
NEXTAUTH_URLmatches the exact production domain includinghttps:// - LCP image has
priorityprop onnext/image -
robots.txtandsitemap.xmlare 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/authand 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 includenode_modulesand be hundreds of MB larger - Using
npm run buildwithout runningprisma generatefirst — Prisma client will be missing in the Docker image - Setting
NEXT_PUBLIC_prefix on secrets likeDATABASE_URL— this exposes them to the browser - Not setting
NEXTAUTH_URLin 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
.envfiles 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'innext.config.tsis 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_SECRETenvironment variable - Run
prisma migrate deploy(notmigrate dev) in CI/CD pipelines —migrate devis for local development only ?connection_limit=1inDATABASE_URLprevents Prisma from overwhelming serverless database connections- Security headers (
X-Frame-Options,X-Content-Type-Options) should be set invercel.jsonornext.config.ts
Advertisement