Skip to main content

Chapter 10: Deployment and Production

What is Deployment in Next.js?

Deployment in Next.js refers to the process of making your application available to users in a production environment. Next.js applications can be deployed to various platforms, each offering different benefits for scalability, performance, and cost.

Deployment Options:

  • Vercel: The creators of Next.js, optimized for Next.js applications
  • Netlify: Popular platform with excellent static site support
  • AWS: Amazon Web Services with various deployment options
  • Docker: Containerized deployment for any platform
  • Traditional Hosting: VPS, dedicated servers, or shared hosting

Why Choose Different Deployment Platforms?

Vercel:

  • Next.js Optimized: Built specifically for Next.js
  • Zero Configuration: Automatic deployments from Git
  • Edge Functions: Global edge network for fast performance
  • Preview Deployments: Automatic previews for pull requests

Netlify:

  • Static Site Focus: Excellent for SSG applications
  • Form Handling: Built-in form processing
  • Edge Functions: Serverless functions at the edge
  • Split Testing: A/B testing capabilities

AWS:

  • Scalability: Handle any amount of traffic
  • Cost Control: Pay only for what you use
  • Integration: Works with other AWS services
  • Customization: Full control over infrastructure

Docker:

  • Consistency: Same environment across all platforms
  • Portability: Deploy anywhere that supports containers
  • Scalability: Easy horizontal scaling
  • Isolation: Secure and isolated environments

How to Deploy Next.js Applications

1. Vercel Deployment

Automatic Deployment from Git:

# Install Vercel CLI
npm i -g vercel

# Login to Vercel
vercel login

# Deploy from your project directory
vercel

# Deploy to production
vercel --prod

Vercel Configuration:

// vercel.json
{
"framework": "nextjs",
"buildCommand": "npm run build",
"outputDirectory": ".next",
"installCommand": "npm install",
"devCommand": "npm run dev",
"functions": {
"pages/api/**/*.js": {
"runtime": "nodejs18.x"
}
},
"env": {
"DATABASE_URL": "@database-url",
"NEXTAUTH_SECRET": "@nextauth-secret"
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{
"key": "Access-Control-Allow-Origin",
"value": "*"
},
{
"key": "Access-Control-Allow-Methods",
"value": "GET, POST, PUT, DELETE, OPTIONS"
}
]
}
],
"redirects": [
{
"source": "/old-page",
"destination": "/new-page",
"permanent": true
}
],
"rewrites": [
{
"source": "/api/external/:path*",
"destination": "https://external-api.com/:path*"
}
]
}

Environment Variables Setup:

# Set environment variables in Vercel dashboard or CLI
vercel env add DATABASE_URL
vercel env add NEXTAUTH_SECRET
vercel env add API_KEY

# Pull environment variables locally
vercel env pull .env.local

Custom Domain Configuration:

# Add custom domain
vercel domains add yourdomain.com

# Configure DNS
# Add CNAME record pointing to cname.vercel-dns.com

2. Netlify Deployment

Netlify Configuration:

# netlify.toml
[build]
command = "npm run build"
publish = ".next"

[build.environment]
NODE_VERSION = "18"

[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200

[[headers]]
for = "/api/*"
[headers.values]
Access-Control-Allow-Origin = "*"
Access-Control-Allow-Methods = "GET, POST, PUT, DELETE, OPTIONS"

[functions]
directory = "netlify/functions"

Netlify Functions:

// netlify/functions/api.js
exports.handler = async (event, context) => {
const { httpMethod, path, body, headers } = event

// Handle CORS
if (httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
},
}
}

// Handle API logic
try {
const result = await handleApiRequest(httpMethod, path, body, headers)

return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
body: JSON.stringify(result),
}
} catch (error) {
return {
statusCode: 500,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ error: error.message }),
}
}
}

3. Docker Deployment

Dockerfile for Next.js:

# Dockerfile
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1

RUN yarn build

# Production image, copy all the files and run next
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

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
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"]

Docker Compose:

# docker-compose.yml
version: '3.8'

services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:password@db:5432/mydb
depends_on:
- db
volumes:
- ./.env.local:/app/.env.local:ro

db:
image: postgres:15
environment:
- POSTGRES_DB=mydb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"

nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
- app

volumes:
postgres_data:

Nginx Configuration:

# nginx.conf
events {
worker_connections 1024;
}

http {
upstream nextjs_upstream {
server app:3000;
}

server {
listen 80;
server_name yourdomain.com;

location / {
proxy_pass http://nextjs_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}

# Static files caching
location /_next/static/ {
proxy_pass http://nextjs_upstream;
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
}

4. AWS Deployment

AWS Amplify:

# amplify.yml
version: 1
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*
- .next/cache/**/*

AWS Lambda with Serverless:

# serverless.yml
service: nextjs-app

provider:
name: aws
runtime: nodejs18.x
region: us-east-1
environment:
NODE_ENV: production

functions:
app:
handler: server.handler
events:
- http:
path: /{proxy+}
method: ANY
- http:
path: /
method: ANY

plugins:
- serverless-nextjs-plugin

5. CI/CD Pipeline

GitHub Actions:

# .github/workflows/deploy.yml
name: Deploy to Production

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Run linting
run: npm run lint

- name: Build application
run: npm run build

deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- uses: actions/checkout@v3

- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'

GitLab CI:

# .gitlab-ci.yml
stages:
- test
- build
- deploy

variables:
NODE_VERSION: "18"

test:
stage: test
image: node:${NODE_VERSION}-alpine
script:
- npm ci
- npm test
- npm run lint
only:
- merge_requests
- main

build:
stage: build
image: node:${NODE_VERSION}-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- .next/
expire_in: 1 hour
only:
- main

deploy:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache curl
- curl -X POST $DEPLOY_WEBHOOK_URL
only:
- main
when: manual

Production Optimization

1. Performance Monitoring

Vercel Analytics:

// pages/_app.js
import { Analytics } from '@vercel/analytics/react'

export default function MyApp({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<Analytics />
</>
)
}

Custom Performance Monitoring:

// lib/analytics.js
export function trackPageView(url) {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', process.env.NEXT_PUBLIC_GA_ID, {
page_path: url,
})
}
}

export function trackEvent(action, category, label, value) {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
})
}
}

// pages/_app.js
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { trackPageView } from '../lib/analytics'

export default function MyApp({ Component, pageProps }) {
const router = useRouter()

useEffect(() => {
const handleRouteChange = (url) => {
trackPageView(url)
}

router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])

return <Component {...pageProps} />
}

2. Error Tracking

Sentry Integration:

npm install @sentry/nextjs
// sentry.client.config.js
import * as Sentry from '@sentry/nextjs'

Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
debug: false,
environment: process.env.NODE_ENV,
})

// sentry.server.config.js
import * as Sentry from '@sentry/nextjs'

Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
debug: false,
environment: process.env.NODE_ENV,
})

// sentry.edge.config.js
import * as Sentry from '@sentry/nextjs'

Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
debug: false,
environment: process.env.NODE_ENV,
})

3. Security Headers

Security Configuration:

// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains',
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;",
},
],
},
]
},
}

Best Practices

1. Environment Management

// ✅ Good: Proper environment variable handling
const config = {
apiUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
databaseUrl: process.env.DATABASE_URL,
secret: process.env.NEXTAUTH_SECRET,
}

// Validate required environment variables
const requiredEnvVars = ['DATABASE_URL', 'NEXTAUTH_SECRET']
const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar])

if (missingEnvVars.length > 0) {
throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`)
}

2. Build Optimization

// ✅ Good: Optimize build process
// next.config.js
module.exports = {
experimental: {
optimizeCss: true,
optimizePackageImports: ['@mui/material', 'lodash'],
},

webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
config.optimization.splitChunks.cacheGroups = {
...config.optimization.splitChunks.cacheGroups,
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
}
}
return config
},
}

3. Monitoring and Alerts

// ✅ Good: Set up monitoring
export default function handler(req, res) {
const start = Date.now()

try {
// Your API logic
const result = processRequest(req)

// Log successful requests
console.log(`API request completed in ${Date.now() - start}ms`)

res.status(200).json(result)
} catch (error) {
// Log errors
console.error('API Error:', error)

// Send to monitoring service
if (process.env.NODE_ENV === 'production') {
sendToMonitoring(error, { duration: Date.now() - start })
}

res.status(500).json({ error: 'Internal server error' })
}
}

Common Mistakes to Avoid

1. Exposing Sensitive Data

// ❌ Wrong: Exposing sensitive data in client-side code
const API_KEY = process.env.API_KEY // This will be exposed to the client

// ✅ Correct: Use NEXT_PUBLIC_ prefix only for public variables
const API_URL = process.env.NEXT_PUBLIC_API_URL
const API_KEY = process.env.API_KEY // Server-side only

2. Not Optimizing Images

// ❌ Wrong: Using regular img tags
<img src="/large-image.jpg" alt="Large image" />

// ✅ Correct: Using Next.js Image component
import Image from 'next/image'
<Image src="/large-image.jpg" alt="Large image" width={800} height={600} />

3. Ignoring Error Handling

// ❌ Wrong: No error handling in production
export default function handler(req, res) {
const data = processRequest(req)
res.status(200).json(data)
}

// ✅ Correct: Proper error handling
export default function handler(req, res) {
try {
const data = processRequest(req)
res.status(200).json(data)
} catch (error) {
console.error('Error:', error)
res.status(500).json({ error: 'Internal server error' })
}
}

Summary

Deployment and production optimization are crucial for successful Next.js applications:

  • Choose the right platform based on your needs and budget
  • Implement CI/CD pipelines for automated deployments
  • Monitor performance and errors in production
  • Optimize for security with proper headers and configurations
  • Use proper environment management for different stages

Key Takeaways:

  • Vercel is optimized for Next.js applications
  • Docker provides consistency across environments
  • CI/CD pipelines automate deployment processes
  • Monitor performance and errors in production
  • Implement proper security measures

This tutorial is part of the comprehensive Next.js learning path at syscook.dev. Continue to the next chapter to learn about SEO optimization.

Author: syscook.dev
Last Updated: December 2024