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