Skip to main content

Deployment & Production

Build optimization, deployment strategies, hosting platforms, and production considerations for Next.js applications.

Build and Production

Build Commands

# Development build
npm run dev

# Production build
npm run build

# Start production server
npm run start

# Export static site
npm run build && npm run export

# Analyze bundle
npm run build -- --analyze

Build Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Build output
output: 'standalone', // or 'export'

// Compress output
compress: true,

// Generate ETags
generateEtags: true,

// Optimize images
images: {
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 60,
},

// Experimental features
experimental: {
optimizeCss: true,
optimizePackageImports: ['lodash', 'date-fns'],
},

// Custom webpack config
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// Production optimizations
if (!dev && !isServer) {
config.resolve.alias = {
...config.resolve.alias,
react: 'preact/compat',
'react-dom': 'preact/compat',
};
}

return config;
},

// Environment variables
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},

// Headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
],
},
];
},
};

module.exports = nextConfig;

Environment Variables

# .env.local (development)
NEXT_PUBLIC_API_URL=http://localhost:3000/api
DATABASE_URL=postgresql://localhost:5432/myapp_dev
SECRET_KEY=dev-secret-key

# .env.production (production)
NEXT_PUBLIC_API_URL=https://api.myapp.com
DATABASE_URL=postgresql://prod-server:5432/myapp_prod
SECRET_KEY=prod-secret-key

# .env (all environments)
NEXT_PUBLIC_APP_NAME=My App
NEXT_PUBLIC_VERSION=1.0.0
// app/lib/config.ts
export const config = {
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
databaseUrl: process.env.DATABASE_URL!,
secretKey: process.env.SECRET_KEY!,
appName: process.env.NEXT_PUBLIC_APP_NAME!,
version: process.env.NEXT_PUBLIC_VERSION!,
isDev: process.env.NODE_ENV === 'development',
isProd: process.env.NODE_ENV === 'production',
};

Hosting Platforms

Vercel Deployment

# Install Vercel CLI
npm install -g vercel

# Deploy
vercel

# Deploy to production
vercel --prod

# Set environment variables
vercel env add SECRET_KEY
vercel env add DATABASE_URL
// vercel.json
{
"buildCommand": "npm run build",
"outputDirectory": ".next",
"devCommand": "npm run dev",
"installCommand": "npm install",
"framework": "nextjs",
"regions": ["iad1", "sfo1"],
"functions": {
"app/api/**/*.ts": {
"maxDuration": 30
}
},
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
],
"rewrites": [
{
"source": "/api/(.*)",
"destination": "/api/$1"
}
]
}

Netlify Deployment

# Install Netlify CLI
npm install -g netlify-cli

# Deploy
netlify deploy

# Deploy to production
netlify deploy --prod
# netlify.toml
[build]
command = "npm run build"
publish = ".next"

[build.environment]
NODE_VERSION = "18"
NPM_VERSION = "9"

[[plugins]]
package = "@netlify/plugin-nextjs"

[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"

[[redirects]]
from = "/old-page"
to = "/new-page"
status = 301

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/**/*

Docker Deployment

# 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

COPY package.json package-lock.json ./
RUN npm ci

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

ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run 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

# 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

CMD ["node", "server.js"]
# docker-compose.yml
version: '3.8'

services:
nextjs:
build:
context: .
dockerfile: Dockerfile
ports:
- '3000:3000'
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
depends_on:
- db
restart: unless-stopped

db:
image: postgres:13
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped

volumes:
postgres_data:

Performance Monitoring

Analytics Integration

// app/lib/analytics.ts
declare global {
interface Window {
gtag: (command: string, ...args: any[]) => void;
}
}

export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;

export function pageview(url: string) {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', GA_TRACKING_ID, {
page_path: url,
});
}
}

export function event(
action: string,
category: string,
label?: string,
value?: number
) {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
});
}
}
// app/components/Analytics.tsx
'use client';

import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import { pageview } from '@/lib/analytics';

export default function Analytics() {
const pathname = usePathname();

useEffect(() => {
pageview(pathname);
}, [pathname]);

return null;
}

Error Tracking

// app/lib/errorTracking.ts
export function logError(error: Error, context?: any) {
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Error:', error);
console.error('Context:', context);
}

// Send to error tracking service
if (typeof window !== 'undefined') {
// Sentry, LogRocket, etc.
// Sentry.captureException(error, { extra: context });
}
}

export function logEvent(event: string, data?: any) {
// Log to analytics service
if (typeof window !== 'undefined') {
// analytics.track(event, data);
}
}

Security Considerations

Content Security Policy

// app/lib/csp.ts
export function generateCSP() {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');

const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`font-src 'self'`,
`connect-src 'self' https://api.example.com`,
`frame-src 'none'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
].join('; ');

return { csp, nonce };
}

Security Headers

// next.config.js
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
];

module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};

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: Type check
run: npm run type-check

- name: Build application
run: npm run build
env:
NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}

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'

Docker CI/CD

# .github/workflows/docker.yml
name: Docker Build and Deploy

on:
push:
branches: [main]

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

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max

Database and Storage

Database Connection

// app/lib/db.ts
import { Pool } from 'pg';

const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl:
process.env.NODE_ENV === 'production'
? { rejectUnauthorized: false }
: false,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});

export async function query(text: string, params?: any[]) {
const client = await pool.connect();
try {
const result = await client.query(text, params);
return result;
} finally {
client.release();
}
}

File Storage

// app/lib/storage.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});

export async function uploadFile(file: File, key: string) {
const buffer = await file.arrayBuffer();

const command = new PutObjectCommand({
Bucket: process.env.AWS_BUCKET_NAME,
Key: key,
Body: buffer,
ContentType: file.type,
});

return await s3Client.send(command);
}

Health Checks and Monitoring

Health Check Endpoint

// app/api/health/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
try {
// Check database connection
// Check external APIs
// Check file system

return NextResponse.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version,
});
} catch (error) {
return NextResponse.json(
{
status: 'unhealthy',
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date().toISOString(),
},
{ status: 503 }
);
}
}

Logging

// app/lib/logger.ts
import pino from 'pino';

const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport:
process.env.NODE_ENV === 'development'
? {
target: 'pino-pretty',
options: {
colorize: true,
},
}
: undefined,
});

export default logger;

Quick Reference

Build Commands

  • npm run build - Production build
  • npm run start - Start production server
  • npm run export - Export static site
  • npm run analyze - Analyze bundle size

Environment Variables

  • NEXT_PUBLIC_* - Client-side accessible
  • NODE_ENV - Environment (development/production)
  • DATABASE_URL - Database connection
  • SECRET_KEY - Server-side secrets

Deployment Platforms

  • Vercel: Native Next.js platform
  • Netlify: JAMstack platform
  • AWS Amplify: Full-stack platform
  • Docker: Container deployment

Performance Monitoring

  • Core Web Vitals tracking
  • Error boundaries and logging
  • Analytics integration
  • Health check endpoints

Security Headers

  • Content Security Policy (CSP)
  • X-Frame-Options
  • X-Content-Type-Options
  • Strict-Transport-Security

Best Practices

  • Use environment variables for configuration
  • Implement proper error handling
  • Set up monitoring and logging
  • Use CDN for static assets
  • Implement health checks
  • Use SSL/TLS in production
  • Regular security updates
  • Monitor performance metrics