Next.js Deployment
Next.js can be deployed anywhere that supports Node.js. The most common options are Vercel (the platform built by the creators of Next.js), self-hosted Node.js servers, Docker containers, and serverless platforms. Each approach has different tradeoffs for complexity, cost, and control.
This guide covers all major deployment strategies, from zero-config Vercel deployments to fully self-managed Docker setups.
info
Vercel is the recommended deployment platform for Next.js. It provides zero-config deployment, automatic preview URLs for PRs, edge functions, and deep integration with Next.js features.
| 1 | # Install Vercel CLI |
| 2 | npm i -g vercel |
| 3 | |
| 4 | # Deploy (interactive setup) |
| 5 | vercel |
| 6 | |
| 7 | # Deploy to production |
| 8 | vercel --prod |
| 9 | |
| 10 | # Deploy with environment variables |
| 11 | vercel --prod -e DATABASE_URL=postgresql://... -e API_KEY=sk-... |
| 12 | |
| 13 | # Link to an existing project |
| 14 | vercel link |
| 15 | vercel deploy --prod |
Or deploy via the Vercel dashboard:
Configure Next.js for production in next.config.ts. Key options include output mode, image optimization, headers, and redirects.
| 1 | // next.config.ts |
| 2 | import type { NextConfig } from "next"; |
| 3 | |
| 4 | const nextConfig: NextConfig = { |
| 5 | // Output mode for self-hosted deployments |
| 6 | output: "standalone", |
| 7 | |
| 8 | // Image optimization |
| 9 | images: { |
| 10 | remotePatterns: [ |
| 11 | { |
| 12 | protocol: "https", |
| 13 | hostname: "**.example.com", |
| 14 | }, |
| 15 | { |
| 16 | protocol: "https", |
| 17 | hostname: "images.unsplash.com", |
| 18 | }, |
| 19 | ], |
| 20 | formats: ["image/avif", "image/webp"], |
| 21 | minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days |
| 22 | }, |
| 23 | |
| 24 | // Security headers |
| 25 | async headers() { |
| 26 | return [ |
| 27 | { |
| 28 | source: "/(.*)", |
| 29 | headers: [ |
| 30 | { key: "X-Frame-Options", value: "DENY" }, |
| 31 | { key: "X-Content-Type-Options", value: "nosniff" }, |
| 32 | { key: "Referrer-Policy", value: "origin-when-cross-origin" }, |
| 33 | { |
| 34 | key: "Content-Security-Policy", |
| 35 | value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline';", |
| 36 | }, |
| 37 | ], |
| 38 | }, |
| 39 | ]; |
| 40 | }, |
| 41 | |
| 42 | // Redirects |
| 43 | async redirects() { |
| 44 | return [ |
| 45 | { source: "/old-blog/:slug", destination: "/blog/:slug", permanent: true }, |
| 46 | { source: "/home", destination: "/", permanent: true }, |
| 47 | ]; |
| 48 | }, |
| 49 | |
| 50 | // Rewrites (proxy) |
| 51 | async rewrites() { |
| 52 | return [ |
| 53 | { |
| 54 | source: "/api/legacy/:path*", |
| 55 | destination: "https://legacy-api.example.com/:path*", |
| 56 | }, |
| 57 | ]; |
| 58 | }, |
| 59 | |
| 60 | // Experimental features |
| 61 | experimental: { |
| 62 | // Enable PPR (Partial Prerendering) |
| 63 | ppr: true, |
| 64 | }, |
| 65 | }; |
| 66 | |
| 67 | export default nextConfig; |
The standalone output mode creates a minimal deployment package that includes only the files needed to run your app. No node_modules directory is required in production โ all dependencies are bundled into the output.
| 1 | # Build with standalone output |
| 2 | npm run build |
| 3 | |
| 4 | # The standalone output is in .next/standalone/ |
| 5 | # It includes a minimal Node.js server |
| 6 | ls .next/standalone/ |
| 7 | |
| 8 | # Copy static assets and public files |
| 9 | cp -r .next/static .next/standalone/.next/static |
| 10 | cp -r public .next/standalone/public |
| 11 | |
| 12 | # Start the server |
| 13 | node .next/standalone/server.js |
| 1 | // next.config.ts โ Enable standalone output |
| 2 | const nextConfig: NextConfig = { |
| 3 | output: "standalone", |
| 4 | }; |
info
Deploy Next.js in a Docker container for consistent, reproducible environments. Use the standalone output mode for minimal image size.
| 1 | # Dockerfile for Next.js standalone output |
| 2 | |
| 3 | # Stage 1: Dependencies |
| 4 | FROM node:20-alpine AS deps |
| 5 | WORKDIR /app |
| 6 | COPY package.json package-lock.json ./ |
| 7 | RUN npm ci --only=production |
| 8 | |
| 9 | # Stage 2: Build |
| 10 | FROM node:20-alpine AS builder |
| 11 | WORKDIR /app |
| 12 | COPY --from=deps /app/node_modules ./node_modules |
| 13 | COPY . . |
| 14 | |
| 15 | # Set build-time environment variables |
| 16 | ENV NEXT_TELEMETRY_DISABLED=1 |
| 17 | ENV NODE_ENV=production |
| 18 | |
| 19 | RUN npm run build |
| 20 | |
| 21 | # Stage 3: Production |
| 22 | FROM node:20-alpine AS runner |
| 23 | WORKDIR /app |
| 24 | |
| 25 | ENV NODE_ENV=production |
| 26 | ENV NEXT_TELEMETRY_DISABLED=1 |
| 27 | |
| 28 | RUN addgroup --system --gid 1001 nodejs |
| 29 | RUN adduser --system --uid 1001 nextjs |
| 30 | |
| 31 | # Copy standalone output |
| 32 | COPY --from=builder /app/.next/standalone ./ |
| 33 | COPY --from=builder /app/.next/static ./.next/static |
| 34 | COPY --from=builder /app/public ./public |
| 35 | |
| 36 | USER nextjs |
| 37 | |
| 38 | EXPOSE 3000 |
| 39 | ENV PORT=3000 |
| 40 | ENV HOSTNAME="0.0.0.0" |
| 41 | |
| 42 | CMD ["node", "server.js"] |
| 1 | # docker-compose.yml |
| 2 | services: |
| 3 | app: |
| 4 | build: . |
| 5 | ports: |
| 6 | - "3000:3000" |
| 7 | environment: |
| 8 | - DATABASE_URL=postgresql://user:pass@db:5432/mydb |
| 9 | - NEXT_PUBLIC_API_URL=https://api.example.com |
| 10 | depends_on: |
| 11 | - db |
| 12 | |
| 13 | db: |
| 14 | image: postgres:16-alpine |
| 15 | environment: |
| 16 | POSTGRES_USER: user |
| 17 | POSTGRES_PASSWORD: pass |
| 18 | POSTGRES_DB: mydb |
| 19 | volumes: |
| 20 | - postgres_data:/var/lib/postgresql/data |
| 21 | |
| 22 | volumes: |
| 23 | postgres_data: |
best practice
Deploy Next.js on your own server (VPS, bare metal, or VM) with a process manager like PM2 for production reliability.
| 1 | # Build the application |
| 2 | npm run build |
| 3 | |
| 4 | # Start with PM2 (process manager) |
| 5 | npm i -g pm2 |
| 6 | |
| 7 | # Start the production server |
| 8 | pm2 start npm --name "nextjs-app" -- start |
| 9 | |
| 10 | # Or start with standalone output |
| 11 | pm2 start .next/standalone/server.js --name "nextjs-app" |
| 12 | |
| 13 | # Save PM2 process list (survives server restarts) |
| 14 | pm2 save |
| 15 | |
| 16 | # Set up PM2 to start on boot |
| 17 | pm2 startup |
| 18 | |
| 19 | # Useful PM2 commands |
| 20 | pm2 status # View running processes |
| 21 | pm2 logs nextjs-app # View logs |
| 22 | pm2 restart nextjs-app # Restart |
| 23 | pm2 stop nextjs-app # Stop |
| 24 | pm2 delete nextjs-app # Remove |
| 1 | # /etc/nginx/conf.d/nextjs.conf |
| 2 | # Nginx reverse proxy for Next.js |
| 3 | |
| 4 | upstream nextjs { |
| 5 | server 127.0.0.1:3000; |
| 6 | } |
| 7 | |
| 8 | server { |
| 9 | listen 80; |
| 10 | server_name example.com; |
| 11 | |
| 12 | # Redirect HTTP to HTTPS |
| 13 | return 301 https://$server_name$request_uri; |
| 14 | } |
| 15 | |
| 16 | server { |
| 17 | listen 443 ssl http2; |
| 18 | server_name example.com; |
| 19 | |
| 20 | ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; |
| 21 | ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; |
| 22 | |
| 23 | # Security headers |
| 24 | add_header X-Frame-Options "SAMEORIGIN" always; |
| 25 | add_header X-Content-Type-Options "nosniff" always; |
| 26 | |
| 27 | # Proxy to Next.js |
| 28 | location / { |
| 29 | proxy_pass http://nextjs; |
| 30 | proxy_http_version 1.1; |
| 31 | proxy_set_header Upgrade $http_upgrade; |
| 32 | proxy_set_header Connection "upgrade"; |
| 33 | proxy_set_header Host $host; |
| 34 | proxy_set_header X-Real-IP $remote_addr; |
| 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 36 | proxy_set_header X-Forwarded-Proto $scheme; |
| 37 | proxy_cache_bypass $http_upgrade; |
| 38 | } |
| 39 | |
| 40 | # Cache static assets |
| 41 | location /_next/static/ { |
| 42 | proxy_pass http://nextjs; |
| 43 | proxy_cache_valid 200 365d; |
| 44 | add_header Cache-Control "public, max-age=31536000, immutable"; |
| 45 | } |
| 46 | |
| 47 | # Cache images |
| 48 | location /images/ { |
| 49 | proxy_pass http://nextjs; |
| 50 | proxy_cache_valid 200 30d; |
| 51 | } |
| 52 | } |
Environment variables must be set at build time and runtime. Some variables are inlined into the client bundle during build, while server-only variables are read at runtime.
| 1 | # .env.production.local (never committed to git) |
| 2 | |
| 3 | # Server-only (available at runtime) |
| 4 | DATABASE_URL=postgresql://prod-user:pass@db.example.com:5432/proddb |
| 5 | API_SECRET=sk-production-key |
| 6 | JWT_SECRET=your-jwt-secret |
| 7 | |
| 8 | # Client-side (inlined at build time) |
| 9 | NEXT_PUBLIC_API_URL=https://api.example.com |
| 10 | NEXT_PUBLIC_SENTRY_DSN=https://xxx@sentry.io/123 |
warning
Set up continuous deployment with GitHub Actions or a similar CI/CD platform.
| 1 | # .github/workflows/deploy.yml |
| 2 | name: Deploy |
| 3 | |
| 4 | on: |
| 5 | push: |
| 6 | branches: [main] |
| 7 | pull_request: |
| 8 | branches: [main] |
| 9 | |
| 10 | jobs: |
| 11 | test: |
| 12 | runs-on: ubuntu-latest |
| 13 | steps: |
| 14 | - uses: actions/checkout@v4 |
| 15 | - uses: actions/setup-node@v4 |
| 16 | with: |
| 17 | node-version: 20 |
| 18 | cache: npm |
| 19 | - run: npm ci |
| 20 | - run: npm run lint |
| 21 | - run: npm run typecheck |
| 22 | - run: npm test |
| 23 | |
| 24 | deploy: |
| 25 | needs: test |
| 26 | if: github.ref == 'refs/heads/main' |
| 27 | runs-on: ubuntu-latest |
| 28 | steps: |
| 29 | - uses: actions/checkout@v4 |
| 30 | - uses: actions/setup-node@v4 |
| 31 | with: |
| 32 | node-version: 20 |
| 33 | cache: npm |
| 34 | - run: npm ci |
| 35 | - run: npm run build |
| 36 | - uses: amondnet/vercel-action@v25 |
| 37 | with: |
| 38 | vercel-token: ${{ secrets.VERCEL_TOKEN }} |
| 39 | vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} |
| 40 | vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} |
| 41 | vercel-args: '--prod' |
Enable Compression
Use Nginx or your hosting platform to enable gzip/brotli compression. Next.js standalone does not compress by default.
CDN for Static Assets
Serve _next/static/ from a CDN with immutable cache headers. These files contain content hashes and can be cached forever.
Database Connection Pooling
Use connection pooling (PgBouncer, Prisma Accelerate, Neon) to handle concurrent database connections efficiently in serverless or multi-instance deployments.
Monitor and Alert
Set up error tracking (Sentry), performance monitoring (Vercel Analytics or custom), and uptime monitoring to catch issues before users do.
best practice